active_configuration 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +133 -0
- data/app/models/active_configuration/setting.rb +35 -0
- data/lib/active_configuration/base.rb +38 -0
- data/lib/active_configuration/engine.rb +4 -0
- data/lib/active_configuration/error.rb +3 -0
- data/lib/active_configuration/option.rb +129 -0
- data/lib/active_configuration/setting_manager.rb +94 -0
- data/lib/active_configuration/setting_proxy.rb +202 -0
- data/lib/active_configuration/table_name.rb +20 -0
- data/lib/active_configuration/version.rb +10 -0
- data/lib/active_configuration.rb +7 -0
- data/lib/active_record/configuration.rb +145 -0
- data/lib/generators/active_configuration/install/install_generator.rb +27 -0
- data/lib/generators/active_configuration/install/templates/create_active_configuration_settings.rb +16 -0
- metadata +186 -0
data/README.md
ADDED
@@ -0,0 +1,133 @@
|
|
1
|
+
# ActiveConfiguration
|
2
|
+
|
3
|
+
ActiveConfiguration is an engine that exposes a generic settings store to
|
4
|
+
ActiveRecord models. Made for very configurable applications, it allows you
|
5
|
+
to avoid implementing specific ways to store settings for each model that
|
6
|
+
needs such a configuration. If your application isn't very configurable,
|
7
|
+
ActiveConfiguration isn't what you want.
|
8
|
+
|
9
|
+
If you had a `Category` model that only had a configurable `sort` attribute,
|
10
|
+
ActiveConfiguration would be overkill. Rather, you would just read and write
|
11
|
+
values using a specific `sort` column and restrict the allowed values using
|
12
|
+
something like `validates_inclusion_of`.
|
13
|
+
|
14
|
+
However, if your `Category` model was more flexible in its configuration, you
|
15
|
+
may want a `sort` setting, a `limit` setting and multiple `price_filter`
|
16
|
+
settings that can be configured by your end user. Without ActiveConfiguration,
|
17
|
+
you would have to develop a way to store and validate these settings for this
|
18
|
+
specific scenario. The `sort` and `limit` settings are simple but because
|
19
|
+
`price_filter` can accept multiple rules, you'd have to set up an additional
|
20
|
+
model. Still, this isn't really an issue when you're dealing with just a single
|
21
|
+
configurable model. When you're dealing with many, things tend to get messy.
|
22
|
+
|
23
|
+
With ActiveConfiguration, all of your settings, even for `price_filter`, can
|
24
|
+
be stored in a generic way. ActiveConfiguration provides a place to store
|
25
|
+
settings for each of your models and even handles validation when you restrict
|
26
|
+
the allowed values or format of an option.
|
27
|
+
|
28
|
+
## Source
|
29
|
+
|
30
|
+
The source for this engine is located at:
|
31
|
+
|
32
|
+
http://github.com/tsmango/active_configuration
|
33
|
+
|
34
|
+
## Installation
|
35
|
+
|
36
|
+
Add the following to your Gemfile:
|
37
|
+
|
38
|
+
gem 'active_configuration'
|
39
|
+
|
40
|
+
Generate the migration for the `settings` table:
|
41
|
+
|
42
|
+
rails g active_configuration:install
|
43
|
+
|
44
|
+
Note: The table can be changed from `settings` to something else by specifying
|
45
|
+
a config option in an initializer like:
|
46
|
+
|
47
|
+
# config/initializers/active_configuration.rb
|
48
|
+
|
49
|
+
Rails.configuration.active_configuration_table_name = 'active_configuration_settings'
|
50
|
+
|
51
|
+
Migrate your database:
|
52
|
+
|
53
|
+
rake db:migrate
|
54
|
+
|
55
|
+
## Example Configuration
|
56
|
+
|
57
|
+
class Category < ActiveRecord::Base
|
58
|
+
configure do
|
59
|
+
option :sort do
|
60
|
+
default 'alphabetical'
|
61
|
+
restrict 'alphabetical', 'manual'
|
62
|
+
end
|
63
|
+
|
64
|
+
option :limit do
|
65
|
+
format 'fixnum'
|
66
|
+
end
|
67
|
+
|
68
|
+
option :price_filter do
|
69
|
+
format 'float'
|
70
|
+
modifiers 'eq', 'lt', 'gt', 'lte', 'gte'
|
71
|
+
multiple true
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
After installing ActiveConfiguration, the #configure block is available to
|
77
|
+
every ActiveRecord model. If the #configure block is defined with a valid
|
78
|
+
configuration, additional methods are made available on the model.
|
79
|
+
|
80
|
+
## Example Usage
|
81
|
+
|
82
|
+
Given we have defined the `Category` class above, instances will now have a #settings
|
83
|
+
method where settings can be read from and written to.
|
84
|
+
|
85
|
+
>> category = Category.create(:name => 'Vinyl Records')
|
86
|
+
=> #<Category id: 1, name: "Vinyl Records", created_at: "2011-08-03 15:46:11", updated_at: "2011-08-03 15:46:11">
|
87
|
+
|
88
|
+
?> category.settings
|
89
|
+
=> #<ActiveConfiguration::SettingManager:0x10e7d1950 @configurable=#<Category id: 1, name: "Vinyl Records", created_at: "2011-08-03 15:46:11", updated_at: "2011-08-03 15:46:11">>
|
90
|
+
|
91
|
+
?> category.settings[:sort]
|
92
|
+
=> {:value=>"alphabetical", :modifier=>nil}
|
93
|
+
|
94
|
+
?> category.settings[:sort][:value]
|
95
|
+
=> "alphabetical"
|
96
|
+
|
97
|
+
?> category.settings[:sort][:value] = 'manual'
|
98
|
+
=> "manual"
|
99
|
+
|
100
|
+
?> category.settings[:price_filter]
|
101
|
+
=> []
|
102
|
+
|
103
|
+
?> category.settings[:price_filter] = [{:modifier => 'gt', :value => 10.00}, {:modifier => 'lte', :value => 25.00}]
|
104
|
+
=> [{:value=>10.0, :modifier=>"gt"}, {:value=>25.0, :modifier=>"lte"}]
|
105
|
+
|
106
|
+
?> category.save
|
107
|
+
=> true
|
108
|
+
|
109
|
+
?> category.settings[:sort][:value]
|
110
|
+
=> "manual"
|
111
|
+
|
112
|
+
?> category.settings[:price_filter]
|
113
|
+
=> [{:value=>10.0, :modifier=>"gt"}, {:value=>25.0, :modifier=>"lte"}]
|
114
|
+
|
115
|
+
Note:
|
116
|
+
|
117
|
+
Settings are only committed after a `save` of the configurable model. If any
|
118
|
+
validation errors should arise, the `save` on the model will return false and
|
119
|
+
errors will be added to the model's errors collection.
|
120
|
+
|
121
|
+
## Testing Environment
|
122
|
+
|
123
|
+
The spec/ directory contains a skeleton Rails 3.0.0 application for testing
|
124
|
+
purposes. All specs can be found in spec/spec/.
|
125
|
+
|
126
|
+
To run the specs, do the following from the root of active\_configuration:
|
127
|
+
|
128
|
+
bundle install --path=vendor/bundles --binstubs
|
129
|
+
bin/rspec spec
|
130
|
+
|
131
|
+
## License
|
132
|
+
|
133
|
+
Copyright © 2011 Thomas Mango, released under the MIT license.
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module ActiveConfiguration
|
2
|
+
|
3
|
+
# Holds the details of a Setting and is attached to any model configured
|
4
|
+
# for use with ActiveConfiguration.
|
5
|
+
#
|
6
|
+
# Note: Settings are not meant to be created and updated directly.
|
7
|
+
# Rather, they should be managed through the #settings method available
|
8
|
+
# through the configured model. See ActiveRecord::Configuration.
|
9
|
+
class Setting < ActiveRecord::Base
|
10
|
+
|
11
|
+
# To avoid collisions with another Setting model that isn't from
|
12
|
+
# ActiveConfiguration, this model and table is namespaced.
|
13
|
+
set_table_name ActiveConfiguration::Config.table_name
|
14
|
+
|
15
|
+
# The model this Setting was created against.
|
16
|
+
belongs_to :configurable, :polymorphic => true
|
17
|
+
|
18
|
+
# Settings are looked up from their key.
|
19
|
+
scope :with_key, lambda { |key|
|
20
|
+
where(:key => key.to_s)
|
21
|
+
}
|
22
|
+
|
23
|
+
# Settings should be created through a configured model's
|
24
|
+
# #active_configuration_settings relationship.
|
25
|
+
attr_protected :configurable_type
|
26
|
+
attr_protected :configurable_id
|
27
|
+
|
28
|
+
# Settings must be related to some other model, have a key
|
29
|
+
# and have a value. They do not necessarily need a modifier.
|
30
|
+
validates_presence_of :configurable_type
|
31
|
+
validates_presence_of :configurable_id
|
32
|
+
validates_presence_of :key
|
33
|
+
validates_presence_of :value
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'active_configuration/option'
|
2
|
+
|
3
|
+
module ActiveConfiguration
|
4
|
+
|
5
|
+
# Holds a set of configuration details. An instance of this object is created
|
6
|
+
# when the #configure block is used on an ActiveRecord model. For more details
|
7
|
+
# see ActiveRecord::Configuration#configure.
|
8
|
+
class Base
|
9
|
+
attr_accessor :options
|
10
|
+
|
11
|
+
# Initializes an empty options hash to store ActiveConfiguration::Option
|
12
|
+
# instances containing configuration details.
|
13
|
+
def initialize
|
14
|
+
@options = HashWithIndifferentAccess.new
|
15
|
+
end
|
16
|
+
|
17
|
+
# Creates and accepts the configuration details for an Option.
|
18
|
+
#
|
19
|
+
# An example of setting an option with a block:
|
20
|
+
#
|
21
|
+
# option :sort do
|
22
|
+
# default 'alphabetical'
|
23
|
+
# restrict 'alphabetical', 'manual'
|
24
|
+
# end
|
25
|
+
#
|
26
|
+
# Here, the #option method is called and passed the key of :sort and then the
|
27
|
+
# the block that follows. The block given to #option is then evaluated against
|
28
|
+
# a new instance of ActiveConfiguration::Option.
|
29
|
+
#
|
30
|
+
# @param [Symbol] key the key for this option and settings against this option.
|
31
|
+
# @param [Proc] block what should be evaluated against the option.
|
32
|
+
def option(key, &block)
|
33
|
+
opt = Option.new(key)
|
34
|
+
opt.instance_eval(&block)
|
35
|
+
@options[key.to_sym] = opt
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
require 'active_configuration/error'
|
2
|
+
|
3
|
+
module ActiveConfiguration
|
4
|
+
|
5
|
+
# Holds the configuration details of a single option. An instance of this
|
6
|
+
# object is created when the #option block is used within a #configure block.
|
7
|
+
class Option
|
8
|
+
|
9
|
+
# ActiveSupport::Callbacks are included so that the #validate! method can
|
10
|
+
# be automaticalled called after each modification to this option.
|
11
|
+
include ActiveSupport::Callbacks
|
12
|
+
|
13
|
+
# There is a callback called :validate that should be watched.
|
14
|
+
define_callbacks :validate
|
15
|
+
|
16
|
+
# After the :validate callback, execute the #validate! method.
|
17
|
+
set_callback :validate, :after, :validate!
|
18
|
+
|
19
|
+
attr_accessor :key, :default_value, :allowed_format, :allowed_values, :allowed_modifiers, :allow_multiple
|
20
|
+
|
21
|
+
alias :allow_multiple? :allow_multiple
|
22
|
+
|
23
|
+
# Initializes the default values for all deatils of this options. This
|
24
|
+
# includes no default value, no restricted values set, no modifiers,
|
25
|
+
# and a 'string' format.
|
26
|
+
#
|
27
|
+
# @param [Symbol] key the key for this option and settings against this option.
|
28
|
+
def initialize(key)
|
29
|
+
@key = key
|
30
|
+
@default_value = nil
|
31
|
+
@allowed_format = 'string'
|
32
|
+
@allowed_values = nil
|
33
|
+
@allowed_modifiers = nil
|
34
|
+
@allow_multiple = false
|
35
|
+
end
|
36
|
+
|
37
|
+
# Sets the default value for this option. This cannot be used in
|
38
|
+
# conjunction with the multiple options. Additionally, if a set
|
39
|
+
# of allowed values is set with the #restrict method, this default
|
40
|
+
# value must appear in that list of allowed values.
|
41
|
+
#
|
42
|
+
# @param value the value to be used as the default for this option.
|
43
|
+
def default(value)
|
44
|
+
run_callbacks :validate do
|
45
|
+
@default_value = (value.is_a?(Symbol) ? value.to_s : value)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Sets a specific format that the value of this option must conform
|
50
|
+
# to. Allowed formats include: 'string', 'fixnum', 'float', 'boolean',
|
51
|
+
# 'email', 'url' or a /regular expression/.
|
52
|
+
#
|
53
|
+
# @param [String, Regexp] value the format this option must be given in.
|
54
|
+
def format(value)
|
55
|
+
run_callbacks :validate do
|
56
|
+
@allowed_format = (value.is_a?(Symbol) ? value.to_s : value)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Restricts the allowed values of this option to a given list of values.
|
61
|
+
#
|
62
|
+
# Example:
|
63
|
+
#
|
64
|
+
# restrict 'alphabetical', 'manual'
|
65
|
+
#
|
66
|
+
# @param [Array] values the allowsed values for this option.
|
67
|
+
def restrict(*values)
|
68
|
+
run_callbacks :validate do
|
69
|
+
@allowed_values = values.collect{|value| (value.is_a?(Symbol) ? value.to_s : value)}
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# Restricts the allows modifiers of this option to a given list of modifers.
|
74
|
+
#
|
75
|
+
# Example:
|
76
|
+
#
|
77
|
+
# modifiers 'eq', 'lt', 'gt', 'lte', 'gte'
|
78
|
+
#
|
79
|
+
# @param [Array] values the allowed modifiers for this option
|
80
|
+
def modifiers(*values)
|
81
|
+
run_callbacks :validate do
|
82
|
+
@allowed_modifiers = values.collect{|value| (value.is_a?(Symbol) ? value.to_s : value)}
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# Whether or not this option can have multiple settings set against it.
|
87
|
+
#
|
88
|
+
# @param [TrueClass, FalseClass] value either true or false for whether
|
89
|
+
# this option should allow multiple settings or not.
|
90
|
+
def multiple(value)
|
91
|
+
run_callbacks :validate do
|
92
|
+
@allow_multiple = value
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# Validates how the specified configuration options are used with one
|
97
|
+
# another. If an invalid configuration is detected, such as using both
|
98
|
+
# the #default method and setting #multiple to true, an exception of
|
99
|
+
# ActiveConfiguration::Error is raised.
|
100
|
+
#
|
101
|
+
# Note: This method is automatically called after each of the
|
102
|
+
# configuration methods are run.
|
103
|
+
def validate!
|
104
|
+
|
105
|
+
# If both a default value and a list of allowed values are given,
|
106
|
+
# the default value must appear in the list of allowed values.
|
107
|
+
if !@default_value.nil? and !@allowed_values.nil? and !@allowed_values.include?(@default_value)
|
108
|
+
raise ActiveConfiguration::Error, "The default value '#{@default_value}' isn't present in the list of allowed values."
|
109
|
+
end
|
110
|
+
|
111
|
+
# If multiple is set, it must be set to either true or false.
|
112
|
+
if ![TrueClass, FalseClass].include?(@allow_multiple.class)
|
113
|
+
raise ActiveConfiguration::Error, 'The multiple option requires a boolean.'
|
114
|
+
end
|
115
|
+
|
116
|
+
# If a default value is given, multiple must be false.
|
117
|
+
if !@default_value.nil? and @allow_multiple
|
118
|
+
raise ActiveConfiguration::Error, 'The default value cannot be set in combination with the multiple option.'
|
119
|
+
end
|
120
|
+
|
121
|
+
# If a format is specified, it must be an allowed format. This
|
122
|
+
# includes 'string', 'fixnum', 'float', 'boolean', 'email', 'url'
|
123
|
+
# or a /regular exprssion.
|
124
|
+
if !@allowed_format.nil? and !['string', 'fixnum', 'float', 'boolean', 'email', 'url'].include?(@allowed_format) and !@allowed_format.is_a?(Regexp)
|
125
|
+
raise ActiveConfiguration::Error, "The format #{@allowed_format} is not supported."
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
require 'active_configuration/setting_proxy'
|
2
|
+
|
3
|
+
module ActiveConfiguration
|
4
|
+
|
5
|
+
# Returns a SettingProxy object for any option that has been configured.
|
6
|
+
class SettingManager
|
7
|
+
attr_accessor :configurable
|
8
|
+
attr_accessor :settings
|
9
|
+
|
10
|
+
# Initializes this SettingManager and keeps track of what model this
|
11
|
+
# SettingManager is proxying settings for.
|
12
|
+
#
|
13
|
+
# @param [ActiveRecord::Base] configurable the model that hsa been
|
14
|
+
# configured for use with ActiveConfiguration.
|
15
|
+
def initialize(configurable)
|
16
|
+
@configurable = configurable
|
17
|
+
@settings = Hash.new
|
18
|
+
end
|
19
|
+
|
20
|
+
# Provides access to setting details for a setting at the given key.
|
21
|
+
#
|
22
|
+
# @param [Symbol] key the key of the requested setting.
|
23
|
+
#
|
24
|
+
# @return [Hash, Array, NilClass] the Hash or Array of Hashes for the
|
25
|
+
# setting with the given key or nil if there isn't a match.
|
26
|
+
def [](key)
|
27
|
+
if @configurable.class.configuration.options.has_key?(key)
|
28
|
+
@settings[key] ||= SettingProxy.new(self, key)
|
29
|
+
|
30
|
+
return @settings[key].value
|
31
|
+
end
|
32
|
+
|
33
|
+
return nil
|
34
|
+
end
|
35
|
+
|
36
|
+
# Replaces the Hash or Array of Hashes for the setting with the given
|
37
|
+
# key with the given value.
|
38
|
+
#
|
39
|
+
# @param [Symbol] key the key of the requested setting.
|
40
|
+
#
|
41
|
+
# @return [Hash, Array, NilClass] the Hash or Array of Hashes for the
|
42
|
+
# setting with the given key or nil if there isn't a match.
|
43
|
+
def []=(key, value)
|
44
|
+
if @configurable.class.configuration.options.has_key?(key)
|
45
|
+
@settings[key] ||= SettingProxy.new(self, key)
|
46
|
+
@settings[key].replace(value)
|
47
|
+
|
48
|
+
return @settings[key].value
|
49
|
+
end
|
50
|
+
|
51
|
+
return nil
|
52
|
+
end
|
53
|
+
|
54
|
+
# Writes over multiple settings at once.
|
55
|
+
#
|
56
|
+
# @param [Hash] replacement_settings the has of settings to be set.
|
57
|
+
def write_settings(replacement_settings = {})
|
58
|
+
replacement_settings.each_pair do |key, value|
|
59
|
+
self[key] = value
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Runs validations against all settings with pending modificaitons.
|
64
|
+
# Any errors are added to @configurable.errors[:settings].
|
65
|
+
def validate
|
66
|
+
settings.values.collect{|setting| setting.validate}
|
67
|
+
end
|
68
|
+
|
69
|
+
# Saves all settings with pending modificaitons.
|
70
|
+
#
|
71
|
+
# @return [Boolean] whether or not the save was successful.
|
72
|
+
def save
|
73
|
+
return !settings.values.collect{|setting| setting.save}.include?(false)
|
74
|
+
end
|
75
|
+
|
76
|
+
# Writes over multiple settings and saves all setting updates at once.
|
77
|
+
#
|
78
|
+
# @param [Hash] replacement_settings the has of settings to be set.
|
79
|
+
#
|
80
|
+
# @return [Boolean] whether or not the save was successful.
|
81
|
+
def update_settings(replacement_settings = {})
|
82
|
+
write_settings(replacement_settings)
|
83
|
+
|
84
|
+
validate
|
85
|
+
|
86
|
+
return (@configurable.errors[:settings].empty? ? save : false)
|
87
|
+
end
|
88
|
+
|
89
|
+
# Resets any pending setting modifications.
|
90
|
+
def reload
|
91
|
+
@settings = Hash.new
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,202 @@
|
|
1
|
+
require 'active_configuration/error'
|
2
|
+
|
3
|
+
module ActiveConfiguration
|
4
|
+
|
5
|
+
# Handles the reading and writing of ActiveConfiguration::Setting objects
|
6
|
+
# and ensures configuration requirements are upheld.
|
7
|
+
class SettingProxy
|
8
|
+
attr_accessor :manager
|
9
|
+
attr_accessor :key
|
10
|
+
attr_accessor :value
|
11
|
+
|
12
|
+
# Initializes a new ActiveConfiguration::SettingProxy with a related
|
13
|
+
# SettingManager and a key for this setting. This setting's modifiers
|
14
|
+
# and values are cached locally as either a Hash or an Array for later
|
15
|
+
# access and manipulation.
|
16
|
+
#
|
17
|
+
# @param [ActiveConfiguration::SettingManager] manager the manager which
|
18
|
+
# holds this SettingProxy and has access to the configurable object that
|
19
|
+
# this setting will be attached to.
|
20
|
+
# @param [Symbol] key the key for this setting and its related option.
|
21
|
+
def initialize(manager, key)
|
22
|
+
@manager, @key = manager, key
|
23
|
+
|
24
|
+
if settings = @manager.configurable.active_configuration_settings.with_key(@key).all
|
25
|
+
if option.allow_multiple?
|
26
|
+
@value = settings.collect{|setting| {:value => coerce(setting.value), :modifier => setting.modifier}}
|
27
|
+
|
28
|
+
else
|
29
|
+
setting = settings.first
|
30
|
+
|
31
|
+
@value = {
|
32
|
+
:modifier => (setting ? setting.modifier : nil),
|
33
|
+
:value => coerce(setting ? setting.value : option.default_value)
|
34
|
+
}
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Replaces the underlying Hash or Array with a replacement. This handles
|
40
|
+
# reverting to defaults when nil is given as the value.
|
41
|
+
#
|
42
|
+
# Note: Athough Hashes given may contain keys other than :modifier and
|
43
|
+
# :value, all other keys will be stripped out and not saved.
|
44
|
+
#
|
45
|
+
# @raise [ArgumentError] if a Hash, Array or NilClass isn't given for a
|
46
|
+
# multiple option.
|
47
|
+
# @raise [ArgumentError] if a Hash or NilClass isn't given for a non-multiple
|
48
|
+
# option.
|
49
|
+
#
|
50
|
+
# @param [Hash] value_with_modifier the Hash or Array of Hashes containing
|
51
|
+
# modifier and value pairs.
|
52
|
+
#
|
53
|
+
# @return [Hash, Array] the requested change.
|
54
|
+
def replace(value_with_modifier)
|
55
|
+
if option.allow_multiple?
|
56
|
+
if value_with_modifier.is_a?(Hash) or value_with_modifier.is_a?(Array) or value_with_modifier.is_a?(NilClass)
|
57
|
+
value_with_modifier = [value_with_modifier].flatten.collect{|value_with_modifier| {:modifier => nil, :value => nil}.merge(value_with_modifier.nil? ? {} : value_with_modifier.slice(*[:modifier, :value]))}
|
58
|
+
value_with_modifier.delete({:modifier => nil, :value => nil})
|
59
|
+
else
|
60
|
+
raise ArgumentError, "Array expected."
|
61
|
+
end
|
62
|
+
else
|
63
|
+
if value_with_modifier.is_a?(Hash) or value_with_modifier.is_a?(NilClass)
|
64
|
+
value_with_modifier = {:modifier => nil, :value => nil}.merge(value_with_modifier.nil? ? {:value => coerce(option.default_value)} : value_with_modifier.slice(*[:modifier, :value]))
|
65
|
+
else
|
66
|
+
raise ArgumentError, "Hash expected."
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
return (@value = value_with_modifier)
|
71
|
+
end
|
72
|
+
|
73
|
+
# Checks modifiers and values on this setting for validation errors and, if
|
74
|
+
# found, adds those errors to this proxy's model's collection of errors.
|
75
|
+
def validate
|
76
|
+
errors = Array.new
|
77
|
+
|
78
|
+
[value].flatten.each do |value_with_modifier|
|
79
|
+
value = value_with_modifier[:value]
|
80
|
+
modifier = value_with_modifier[:modifier]
|
81
|
+
|
82
|
+
if !option.allowed_values.nil? and !option.allowed_values.include?(value)
|
83
|
+
errors << "The value '#{value}' for the '#{option.key}' setting isn't present in the list of allowed values."
|
84
|
+
end
|
85
|
+
|
86
|
+
if !option.allowed_format.nil?
|
87
|
+
case option.allowed_format
|
88
|
+
when 'string'
|
89
|
+
if !value.is_a?(String)
|
90
|
+
errors << "The value '#{value}' for the '#{option.key}' setting is not a String."
|
91
|
+
end
|
92
|
+
when 'fixnum'
|
93
|
+
if !value.is_a?(Fixnum)
|
94
|
+
errors << "The value '#{value}' for the '#{option.key}' setting is not a Fixnum."
|
95
|
+
end
|
96
|
+
when 'float'
|
97
|
+
if !value.is_a?(Float) and !value.is_a?(Fixnum)
|
98
|
+
errors << "The value '#{value}' for the '#{option.key}' setting is not a Float."
|
99
|
+
end
|
100
|
+
when 'boolean'
|
101
|
+
if !value.is_a?(TrueClass) and !value.is_a?(FalseClass)
|
102
|
+
errors << "The value '#{value}' for the '#{option.key}' setting is not a Boolean."
|
103
|
+
end
|
104
|
+
when 'email'
|
105
|
+
if !value[/^[A-Z0-9_\.%\+\-\']+@(?:[A-Z0-9\-]+\.)+(?:[A-Z]{2,4}|museum|travel)$/i]
|
106
|
+
errors << "The value '#{value}' for the '#{option.key}' setting is not an Email Address."
|
107
|
+
end
|
108
|
+
when 'url'
|
109
|
+
if !value[URI.regexp]
|
110
|
+
errors << "The value '#{value}' for the '#{option.key}' setting is not a URL."
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
if option.allowed_format.is_a?(Regexp) and !value[option.allowed_format]
|
115
|
+
errors << "The value '#{value}' for the '#{option.key}' setting is not in the correct format."
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
if !modifier.nil? and !option.allowed_modifiers.nil? and !option.allowed_modifiers.include?(modifier)
|
120
|
+
errors << "The modifier '#{modifier}' for the '#{option.key}' setting isn't present in the list of allowed modifiers."
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
errors.each do |error|
|
125
|
+
@manager.configurable.errors.add(:settings, error)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
# Saves this setting's modifiers and values.
|
130
|
+
#
|
131
|
+
# @return [Boolean] whether or not the save was successful.
|
132
|
+
def save
|
133
|
+
save_status = true
|
134
|
+
original_setting_ids = @manager.configurable.active_configuration_settings.with_key(@key).collect(&:id)
|
135
|
+
replaced_setting_ids = []
|
136
|
+
|
137
|
+
[value].flatten.each do |value_with_modifier|
|
138
|
+
if (setting = @manager.configurable.active_configuration_settings.create(:key => @key, :modifier => value_with_modifier[:modifier], :value => value_with_modifier[:value])).new_record?
|
139
|
+
save_status = false && break
|
140
|
+
else
|
141
|
+
replaced_setting_ids << setting.id
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
@manager.configurable.active_configuration_settings.reload
|
146
|
+
@manager.configurable.active_configuration_settings.with_key(@key).where(:id => (save_status ? original_setting_ids : replaced_setting_ids)).destroy_all
|
147
|
+
|
148
|
+
@manager.settings.delete(@key)
|
149
|
+
|
150
|
+
return save_status
|
151
|
+
end
|
152
|
+
|
153
|
+
# Returns the Hash or Array representation of the underlying stored settings
|
154
|
+
# depending on whether or not this is a multiple option.
|
155
|
+
#
|
156
|
+
# @return [Hash] the value and modifier, as a Hash, for a non-multiple
|
157
|
+
# option.
|
158
|
+
# @return [Array] the array of hashes containing value and modifiers, like
|
159
|
+
# those returned on a non-multiple options, for a multiple option.
|
160
|
+
def inspect
|
161
|
+
return @value.inspect
|
162
|
+
end
|
163
|
+
|
164
|
+
private
|
165
|
+
|
166
|
+
# Returns this SettingProxy's related Option based on the given key. This
|
167
|
+
# is necessary to ensure all configuration requirements are upheld during
|
168
|
+
# reads and writes.
|
169
|
+
#
|
170
|
+
# @return [ActiveConfiguration::Option] the option for this setting.
|
171
|
+
def option
|
172
|
+
return @manager.configurable.class.configuration.options[key]
|
173
|
+
end
|
174
|
+
|
175
|
+
# Coerces a stored String value into the expected type if an allowed format
|
176
|
+
# is explicitly set on this setting's option.
|
177
|
+
#
|
178
|
+
# Note: Because values are validated against given formats when updated,
|
179
|
+
# it is assumed that they can be properly coerced back to their intended
|
180
|
+
# type.
|
181
|
+
#
|
182
|
+
# @param [String] value the value stored in the database that must be coerced.
|
183
|
+
#
|
184
|
+
# @return [String, Fixnum, Float, TrueClass, FalseClass] the properly coerced
|
185
|
+
# value, assuming the value should be coerced.
|
186
|
+
def coerce(value)
|
187
|
+
if !value.nil? and !option.allowed_format.nil?
|
188
|
+
case option.allowed_format
|
189
|
+
when 'fixnum'
|
190
|
+
value = value.to_i
|
191
|
+
when 'float'
|
192
|
+
value = value.to_f
|
193
|
+
when 'boolean'
|
194
|
+
value = true if (value == 'true' or value == 't')
|
195
|
+
value = false if (value == 'false' or value == 'f')
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
return value
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module ActiveConfiguration
|
2
|
+
|
3
|
+
# Holds the configuration details of this ActiveConfiguration install.
|
4
|
+
class Config
|
5
|
+
|
6
|
+
# Returns the name of the table holding ActiveConfiguration::Settings. This
|
7
|
+
# table defaults to `settings` but can be changed with an initializer like:
|
8
|
+
#
|
9
|
+
# Rails.configuration.active_configuration_table_name = 'active_configuration_settings'
|
10
|
+
#
|
11
|
+
# @return [String] the table name holding ActiveConfiguration::Settings.
|
12
|
+
def self.table_name
|
13
|
+
if Rails.configuration.respond_to?(:active_configuration_table_name)
|
14
|
+
return Rails.configuration.active_configuration_table_name
|
15
|
+
end
|
16
|
+
|
17
|
+
(Rails.configuration.active_configuration_table_name = 'settings')
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,145 @@
|
|
1
|
+
require 'active_configuration/base'
|
2
|
+
require 'active_configuration/setting_manager'
|
3
|
+
require 'active_configuration/error'
|
4
|
+
|
5
|
+
module ActiveRecord
|
6
|
+
|
7
|
+
# Exposes a #configure method to all ActiveRecord classes and if configured,
|
8
|
+
# defines a #settings method for reading and writing settings.
|
9
|
+
module Configuration
|
10
|
+
def self.included(base)
|
11
|
+
base.extend(ClassMethods)
|
12
|
+
end
|
13
|
+
|
14
|
+
module ClassMethods
|
15
|
+
|
16
|
+
# Configures the current ActiveRecord class to allow specific options.
|
17
|
+
# After being configured, a #settings method will be defined against
|
18
|
+
# all instances as well as a has_many :active_configuration_settings
|
19
|
+
# relationship for storing settings.
|
20
|
+
#
|
21
|
+
# Example configuration:
|
22
|
+
#
|
23
|
+
# class Category < ActiveRecord::Base
|
24
|
+
# configure do
|
25
|
+
# option :sort do
|
26
|
+
# default 'alphabetical'
|
27
|
+
# restrict 'alphabetical', 'manual'
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
# option :limit do
|
31
|
+
# format 'fixnum'
|
32
|
+
# end
|
33
|
+
#
|
34
|
+
# option :price_filter do
|
35
|
+
# format 'float'
|
36
|
+
# modifiers 'eq', 'lt', 'gt', 'lte', 'gte'
|
37
|
+
# multiple true
|
38
|
+
# end
|
39
|
+
# end
|
40
|
+
# end
|
41
|
+
#
|
42
|
+
# The #configure block can only contain #option blocks. Within each
|
43
|
+
# option block may be a number of methods such as:
|
44
|
+
#
|
45
|
+
# * default
|
46
|
+
# A default value. Cannot be used in conjunction with multiple.
|
47
|
+
#
|
48
|
+
# * format -
|
49
|
+
# A specific format, including: 'string', 'fixnum', 'float',
|
50
|
+
# 'boolean', 'email', 'url' or a /regular expression/. Defaults
|
51
|
+
# to 'string'.
|
52
|
+
#
|
53
|
+
# * restrict -
|
54
|
+
# An array of allowed values.
|
55
|
+
#
|
56
|
+
# * modifiers -
|
57
|
+
# An array of allowed modifiers.
|
58
|
+
#
|
59
|
+
# * multiple -
|
60
|
+
# Whether or not multiple Settings can be set against the single
|
61
|
+
# option. Must be set to either true or false. Defaults to false.
|
62
|
+
#
|
63
|
+
#
|
64
|
+
# @param [Proc] block the configuration block that contains option blocks.
|
65
|
+
def configure(&block)
|
66
|
+
class_eval <<-EOV
|
67
|
+
|
68
|
+
# Includes the #settings method for reading and writing settings
|
69
|
+
# against any instances of this class.
|
70
|
+
include ActiveRecord::Configuration::InstanceMethods
|
71
|
+
|
72
|
+
# Where the actual settings are stored against the instance.
|
73
|
+
has_many :active_configuration_settings, :as => :configurable, :class_name => 'ActiveConfiguration::Setting'
|
74
|
+
|
75
|
+
# Validates are run on settings along with other validations.
|
76
|
+
validate :validate_settings
|
77
|
+
|
78
|
+
# After being saved, outstanding setting modifications are saved.
|
79
|
+
after_save :save_settings
|
80
|
+
|
81
|
+
# Returns the configuration details of this class.
|
82
|
+
def self.configuration
|
83
|
+
@configuration ||= ActiveConfiguration::Base.new
|
84
|
+
end
|
85
|
+
EOV
|
86
|
+
|
87
|
+
# Evaluates the configuration block given to #configure. Each
|
88
|
+
# option block is then evaluated and options are setup. For more
|
89
|
+
# details, see ActiveConfiguration::Base.
|
90
|
+
configuration.instance_eval(&block)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
module InstanceMethods
|
95
|
+
|
96
|
+
# Returns an ActiveConfiguration::SettingManager that proxies
|
97
|
+
# all reads and writes of settings to ActiveConfiguration::SettingProxy
|
98
|
+
# objects for the specific setting requested.
|
99
|
+
def settings
|
100
|
+
@setting_manager ||= ActiveConfiguration::SettingManager.new(self)
|
101
|
+
end
|
102
|
+
|
103
|
+
# Writes over multiple settings at once.
|
104
|
+
#
|
105
|
+
# @param [Hash] replacement_settings the has of settings to be set.
|
106
|
+
def settings=(replacement_settings = {})
|
107
|
+
settings.write_settings(replacement_settings)
|
108
|
+
end
|
109
|
+
|
110
|
+
# Runs validations against all settings with pending modificaitons.
|
111
|
+
# Any errors are added to #errors[:settings].
|
112
|
+
def validate_settings
|
113
|
+
settings.validate
|
114
|
+
end
|
115
|
+
|
116
|
+
# Saves all settings with pending modificaitons.
|
117
|
+
#
|
118
|
+
# @return [Boolean] whether or not the save was successful.
|
119
|
+
def save_settings
|
120
|
+
settings.save
|
121
|
+
end
|
122
|
+
|
123
|
+
# Writes over multiple settings and saves all setting updates at once.
|
124
|
+
#
|
125
|
+
# @param [Hash] replacement_settings the has of settings to be set.
|
126
|
+
#
|
127
|
+
# @return [Boolean] whether or not the save was successful.
|
128
|
+
def update_settings(replacement_settings = {})
|
129
|
+
settings.update_settings(replacement_settings)
|
130
|
+
end
|
131
|
+
|
132
|
+
# Overrides this model's #reload method by first resetting any requested
|
133
|
+
# changes to settings and then continuing to perform a standard #reload.
|
134
|
+
#
|
135
|
+
# Note: Can this be accomplished with a callback after #reload rather
|
136
|
+
# than overriding the #reload method?
|
137
|
+
#
|
138
|
+
# @param options any options that must be passed along to this methods
|
139
|
+
# original #reload method.
|
140
|
+
def reload(options = nil)
|
141
|
+
settings.reload && super(options)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module ActiveConfiguration
|
2
|
+
|
3
|
+
# Generates a migration for the settings table.
|
4
|
+
#
|
5
|
+
# Example:
|
6
|
+
#
|
7
|
+
# rails g active_configuration:install
|
8
|
+
class InstallGenerator < Rails::Generators::Base
|
9
|
+
include Rails::Generators::Migration
|
10
|
+
|
11
|
+
def self.source_root
|
12
|
+
File.join(File.dirname(__FILE__), 'templates')
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.next_migration_number(dirname)
|
16
|
+
if ActiveRecord::Base.timestamped_migrations
|
17
|
+
Time.now.utc.strftime("%Y%m%d%H%M%S")
|
18
|
+
else
|
19
|
+
"%.3d" % (current_migration_number(dirname) + 1)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def create_migration_file
|
24
|
+
migration_template('create_active_configuration_settings.rb', 'db/migrate/create_active_configuration_settings.rb')
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/lib/generators/active_configuration/install/templates/create_active_configuration_settings.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
class CreateActiveConfigurationSettings < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
create_table ActiveConfiguration::Config.table_name do |t|
|
4
|
+
t.string :configurable_type
|
5
|
+
t.integer :configurable_id
|
6
|
+
t.string :key
|
7
|
+
t.string :modifier
|
8
|
+
t.text :value
|
9
|
+
t.timestamps
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.down
|
14
|
+
drop_table ActiveConfiguration::Config.table_name
|
15
|
+
end
|
16
|
+
end
|
metadata
ADDED
@@ -0,0 +1,186 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: active_configuration
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 23
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 1
|
8
|
+
- 0
|
9
|
+
- 0
|
10
|
+
version: 1.0.0
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Thomas Mango
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2011-08-31 00:00:00 Z
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: rails
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - ">="
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
hash: 7
|
29
|
+
segments:
|
30
|
+
- 3
|
31
|
+
- 0
|
32
|
+
- 0
|
33
|
+
version: 3.0.0
|
34
|
+
type: :runtime
|
35
|
+
version_requirements: *id001
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: sqlite3-ruby
|
38
|
+
prerelease: false
|
39
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
40
|
+
none: false
|
41
|
+
requirements:
|
42
|
+
- - ">="
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
hash: 3
|
45
|
+
segments:
|
46
|
+
- 0
|
47
|
+
version: "0"
|
48
|
+
type: :development
|
49
|
+
version_requirements: *id002
|
50
|
+
- !ruby/object:Gem::Dependency
|
51
|
+
name: active_configuration
|
52
|
+
prerelease: false
|
53
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
54
|
+
none: false
|
55
|
+
requirements:
|
56
|
+
- - ">="
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
hash: 3
|
59
|
+
segments:
|
60
|
+
- 0
|
61
|
+
version: "0"
|
62
|
+
type: :development
|
63
|
+
version_requirements: *id003
|
64
|
+
- !ruby/object:Gem::Dependency
|
65
|
+
name: rspec-rails
|
66
|
+
prerelease: false
|
67
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
68
|
+
none: false
|
69
|
+
requirements:
|
70
|
+
- - ">="
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
hash: 3
|
73
|
+
segments:
|
74
|
+
- 0
|
75
|
+
version: "0"
|
76
|
+
type: :development
|
77
|
+
version_requirements: *id004
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: yard
|
80
|
+
prerelease: false
|
81
|
+
requirement: &id005 !ruby/object:Gem::Requirement
|
82
|
+
none: false
|
83
|
+
requirements:
|
84
|
+
- - ">="
|
85
|
+
- !ruby/object:Gem::Version
|
86
|
+
hash: 3
|
87
|
+
segments:
|
88
|
+
- 0
|
89
|
+
version: "0"
|
90
|
+
type: :development
|
91
|
+
version_requirements: *id005
|
92
|
+
- !ruby/object:Gem::Dependency
|
93
|
+
name: activerecord
|
94
|
+
prerelease: false
|
95
|
+
requirement: &id006 !ruby/object:Gem::Requirement
|
96
|
+
none: false
|
97
|
+
requirements:
|
98
|
+
- - ">="
|
99
|
+
- !ruby/object:Gem::Version
|
100
|
+
hash: 7
|
101
|
+
segments:
|
102
|
+
- 3
|
103
|
+
- 0
|
104
|
+
- 0
|
105
|
+
version: 3.0.0
|
106
|
+
type: :runtime
|
107
|
+
version_requirements: *id006
|
108
|
+
- !ruby/object:Gem::Dependency
|
109
|
+
name: activesupport
|
110
|
+
prerelease: false
|
111
|
+
requirement: &id007 !ruby/object:Gem::Requirement
|
112
|
+
none: false
|
113
|
+
requirements:
|
114
|
+
- - ">="
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
hash: 7
|
117
|
+
segments:
|
118
|
+
- 3
|
119
|
+
- 0
|
120
|
+
- 0
|
121
|
+
version: 3.0.0
|
122
|
+
type: :runtime
|
123
|
+
version_requirements: *id007
|
124
|
+
description: |-
|
125
|
+
ActiveConfiguration is an engine that exposes a generic settings store to
|
126
|
+
ActiveRecord models. Made for very configurable applications, it allows you
|
127
|
+
to avoid implementing specific ways to store settings for each model that
|
128
|
+
needs such configuration. If your application isn't very configurable,
|
129
|
+
ActiveConfiguration is probably overkill.
|
130
|
+
email: tsmango@gmail.com
|
131
|
+
executables: []
|
132
|
+
|
133
|
+
extensions: []
|
134
|
+
|
135
|
+
extra_rdoc_files:
|
136
|
+
- README.md
|
137
|
+
files:
|
138
|
+
- app/models/active_configuration/setting.rb
|
139
|
+
- lib/active_configuration.rb
|
140
|
+
- lib/active_configuration/base.rb
|
141
|
+
- lib/active_configuration/engine.rb
|
142
|
+
- lib/active_configuration/error.rb
|
143
|
+
- lib/active_configuration/option.rb
|
144
|
+
- lib/active_configuration/setting_manager.rb
|
145
|
+
- lib/active_configuration/setting_proxy.rb
|
146
|
+
- lib/active_configuration/table_name.rb
|
147
|
+
- lib/active_configuration/version.rb
|
148
|
+
- lib/active_record/configuration.rb
|
149
|
+
- lib/generators/active_configuration/install/install_generator.rb
|
150
|
+
- lib/generators/active_configuration/install/templates/create_active_configuration_settings.rb
|
151
|
+
- README.md
|
152
|
+
homepage: http://github.com/tsmango/active_configuration
|
153
|
+
licenses: []
|
154
|
+
|
155
|
+
post_install_message:
|
156
|
+
rdoc_options: []
|
157
|
+
|
158
|
+
require_paths:
|
159
|
+
- lib
|
160
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
161
|
+
none: false
|
162
|
+
requirements:
|
163
|
+
- - ">="
|
164
|
+
- !ruby/object:Gem::Version
|
165
|
+
hash: 3
|
166
|
+
segments:
|
167
|
+
- 0
|
168
|
+
version: "0"
|
169
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
170
|
+
none: false
|
171
|
+
requirements:
|
172
|
+
- - ">="
|
173
|
+
- !ruby/object:Gem::Version
|
174
|
+
hash: 3
|
175
|
+
segments:
|
176
|
+
- 0
|
177
|
+
version: "0"
|
178
|
+
requirements: []
|
179
|
+
|
180
|
+
rubyforge_project:
|
181
|
+
rubygems_version: 1.8.6
|
182
|
+
signing_key:
|
183
|
+
specification_version: 3
|
184
|
+
summary: A generic settings store for Rails 3.x ActiveRecord models.
|
185
|
+
test_files: []
|
186
|
+
|