active_configuration 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/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
|
+
|