settingcrazy 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/.rvmrc +1 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +245 -0
- data/Rakefile +2 -0
- data/TODO +10 -0
- data/db/safeattributes.db +0 -0
- data/lib/generators/settingcrazy/setting_values_migration/setting_values_migration_generator.rb +18 -0
- data/lib/generators/settingcrazy/setting_values_migration/templates/migration.rb +15 -0
- data/lib/settingcrazy.rb +23 -0
- data/lib/settingcrazy/class_methods.rb +32 -0
- data/lib/settingcrazy/inheritor.rb +31 -0
- data/lib/settingcrazy/instance_methods.rb +20 -0
- data/lib/settingcrazy/namespace.rb +14 -0
- data/lib/settingcrazy/namespaced_settings_proxy.rb +18 -0
- data/lib/settingcrazy/setting_value.rb +11 -0
- data/lib/settingcrazy/settings_proxy.rb +107 -0
- data/lib/settingcrazy/settings_validator.rb +43 -0
- data/lib/settingcrazy/template.rb +2 -0
- data/lib/settingcrazy/template/base.rb +66 -0
- data/lib/settingcrazy/template/enum.rb +19 -0
- data/lib/settingcrazy/version.rb +3 -0
- data/settingcrazy.gemspec +21 -0
- data/spec/inheritance_spec.rb +76 -0
- data/spec/mass_assignment_spec.rb +69 -0
- data/spec/namespaced_settings_proxy_spec.rb +70 -0
- data/spec/settingcrazy_spec.rb +78 -0
- data/spec/settings_proxy_spec.rb +55 -0
- data/spec/settings_validator_spec.rb +121 -0
- data/spec/spec_helper.rb +8 -0
- data/spec/support/models.rb +10 -0
- data/spec/support/models/campaign.rb +12 -0
- data/spec/support/models/case.rb +11 -0
- data/spec/support/models/clever_campaign.rb +13 -0
- data/spec/support/models/duck.rb +13 -0
- data/spec/support/models/farm.rb +10 -0
- data/spec/support/models/note.rb +12 -0
- data/spec/support/models/scenario.rb +13 -0
- data/spec/support/models/setting_value.rb +8 -0
- data/spec/support/models/templated_campaign.rb +11 -0
- data/spec/support/models/templated_scenario.rb +11 -0
- data/spec/support/models/vendor_instance.rb +9 -0
- data/spec/support/templates.rb +3 -0
- data/spec/support/templates/example_campaign_template.rb +24 -0
- data/spec/support/templates/example_template.rb +12 -0
- metadata +161 -0
@@ -0,0 +1,18 @@
|
|
1
|
+
module SettingCrazy
|
2
|
+
class NamespacedSettingsProxy < SettingsProxy
|
3
|
+
def initialize(model, namespace)
|
4
|
+
@model = model
|
5
|
+
@namespace = namespace
|
6
|
+
@template = namespace.template
|
7
|
+
end
|
8
|
+
|
9
|
+
protected
|
10
|
+
def build_value(key, value)
|
11
|
+
@model.setting_values.build(:key => key, :value => value, :namespace => @namespace.name)
|
12
|
+
end
|
13
|
+
|
14
|
+
def setting_values
|
15
|
+
@model.setting_values.select{ |sv| sv.namespace == @namespace.name }
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
module SettingCrazy
|
2
|
+
class SettingsProxy
|
3
|
+
attr_reader :template
|
4
|
+
|
5
|
+
def initialize(model, template)
|
6
|
+
@model = model
|
7
|
+
@template = template
|
8
|
+
@namespaces = model.class._setting_namespaces
|
9
|
+
# TODO: It would probably be a good idea to memoize the NamespacedSettingsProxies
|
10
|
+
end
|
11
|
+
|
12
|
+
def []=(key, value)
|
13
|
+
if @namespaces && namespace = @namespaces[key.to_sym]
|
14
|
+
return NamespacedSettingsProxy.new(@model, namespace).bulk_assign(value)
|
15
|
+
end
|
16
|
+
|
17
|
+
value.reject!(&:blank?) if value.respond_to?(:reject!)
|
18
|
+
sv = setting_record(key)
|
19
|
+
if sv.blank?
|
20
|
+
build_value(key, value)
|
21
|
+
else
|
22
|
+
sv.value = value
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def [](key)
|
27
|
+
if @namespaces && namespace = @namespaces[key.to_sym]
|
28
|
+
return NamespacedSettingsProxy.new(@model, namespace)
|
29
|
+
end
|
30
|
+
sv = setting_record(key)
|
31
|
+
if sv.blank?
|
32
|
+
parent_value(key) || template_default_value(key) || nil
|
33
|
+
else
|
34
|
+
sv.value
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def bulk_assign(attributes)
|
39
|
+
attributes.each do |(k,v)|
|
40
|
+
self[k] = v
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def delete(key)
|
45
|
+
@model.setting_values.delete(setting_record(key))
|
46
|
+
end
|
47
|
+
|
48
|
+
def method_missing(method_name, *args, &block)
|
49
|
+
if method_name =~ /=$/
|
50
|
+
attribute = method_name[0...-1]
|
51
|
+
self[attribute] = args.first
|
52
|
+
else
|
53
|
+
self[method_name]
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def parent_settings
|
58
|
+
return nil unless @model.class._inheritor.present?
|
59
|
+
@model.class._inheritor.parent_settings_for(@model)
|
60
|
+
end
|
61
|
+
|
62
|
+
def each(&block)
|
63
|
+
setting_values.each(&block)
|
64
|
+
end
|
65
|
+
|
66
|
+
def map(&block)
|
67
|
+
setting_values.map(&block)
|
68
|
+
end
|
69
|
+
|
70
|
+
def inspect
|
71
|
+
@model.reload unless @model.new_record?
|
72
|
+
self.to_hash.inspect
|
73
|
+
end
|
74
|
+
|
75
|
+
def to_hash
|
76
|
+
setting_values.inject({}) do |hash, sv|
|
77
|
+
hash[sv.key] = sv.value
|
78
|
+
hash
|
79
|
+
end.symbolize_keys
|
80
|
+
end
|
81
|
+
|
82
|
+
protected
|
83
|
+
def template_default_value(key)
|
84
|
+
template.present? ? template.defaults[key] : nil
|
85
|
+
end
|
86
|
+
|
87
|
+
def parent_value(key)
|
88
|
+
parent_settings.present? ? parent_settings[key] : nil
|
89
|
+
end
|
90
|
+
|
91
|
+
def setting_record(attribute)
|
92
|
+
# Check valid template attrs
|
93
|
+
if template.present? && !template.valid_option?(attribute)
|
94
|
+
raise ActiveRecord::UnknownAttributeError
|
95
|
+
end
|
96
|
+
setting_values.select{|sv| sv.key.to_sym == attribute.to_sym }.last # When updating an existing setting_value, the new value comes after the existing value in the array.
|
97
|
+
end
|
98
|
+
|
99
|
+
def build_value(key, value)
|
100
|
+
@model.setting_values.build(:key => key, :value => value)
|
101
|
+
end
|
102
|
+
|
103
|
+
def setting_values
|
104
|
+
@model.setting_values
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
class SettingsValidator < ActiveModel::Validator
|
2
|
+
attr_accessor :record, :settings, :template
|
3
|
+
|
4
|
+
def validate(record)
|
5
|
+
self.record = record
|
6
|
+
self.settings = record.settings
|
7
|
+
self.template = settings.template
|
8
|
+
|
9
|
+
if record.persisted? && template.present? # Not to valid setting_values for unsaved owner & Validate only if the template exists
|
10
|
+
template.enums.symbolize_keys.each do |key, name_value_pairs|
|
11
|
+
enum_options = template.enum_options(key)
|
12
|
+
current_value = settings.send(key)
|
13
|
+
|
14
|
+
validate_presence(key, current_value) if enum_options[:required]
|
15
|
+
validate_singleness(key, current_value) unless enum_options[:multiple]
|
16
|
+
validate_dependent(key, current_value, enum_options[:dependent]) if enum_options[:dependent] && current_value.present?
|
17
|
+
validate_range(key, current_value, name_value_pairs.values) if enum_options[:type] != 'text' && current_value.present?
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
protected
|
23
|
+
def validate_presence(key, value)
|
24
|
+
@record.errors.add key, "Setting, '#{template.name_for(key)}', is required" if value.blank?
|
25
|
+
end
|
26
|
+
|
27
|
+
def validate_singleness(key, value)
|
28
|
+
@record.errors.add key, "Cannot save multiple values for Setting, '#{template.name_for(key)}'" if value.instance_of?(Array)
|
29
|
+
end
|
30
|
+
|
31
|
+
def validate_dependent(key, value, conditions)
|
32
|
+
conditions.each do |dependent_on_key, dependent_on_value|
|
33
|
+
@record.errors.add key, "'#{template.name_for(key)}' can only be specified if '#{template.name_for(dependent_on_key)}' is set to '#{dependent_on_value}'" unless settings.send(dependent_on_key) == dependent_on_value
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def validate_range(key, value, enum_values)
|
38
|
+
values = value.instance_of?(Array) ? value : [value]
|
39
|
+
values.each do |v|
|
40
|
+
@record.errors.add key, "'#{v}' is not a valid setting for '#{template.name_for(key)}'" unless enum_values.include?(v)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module SettingCrazy
|
2
|
+
module Template
|
3
|
+
class Base
|
4
|
+
|
5
|
+
class << self
|
6
|
+
|
7
|
+
def enum(id, name=id.to_s, options={}, &block)
|
8
|
+
enums[id] = SettingCrazy::Template::Enum.new(name, options, &block)
|
9
|
+
end
|
10
|
+
|
11
|
+
def enums
|
12
|
+
@enums ||= {}
|
13
|
+
end
|
14
|
+
|
15
|
+
def enum_options(key)
|
16
|
+
enums[key].options
|
17
|
+
end
|
18
|
+
|
19
|
+
# Returns an array suitable for use in options_for_select helper
|
20
|
+
# Example:
|
21
|
+
#
|
22
|
+
# options_for_select(GoogleSettings::Campaign.options(:platform))
|
23
|
+
#
|
24
|
+
def options(name)
|
25
|
+
enum = enums[name]
|
26
|
+
return [] if enum.blank?
|
27
|
+
enum.to_a
|
28
|
+
end
|
29
|
+
|
30
|
+
def name_for(id)
|
31
|
+
raise "No setting with id '#{id}'" unless enums.has_key?(id)
|
32
|
+
enums[id].name
|
33
|
+
end
|
34
|
+
|
35
|
+
# Returns the available options for the setting object
|
36
|
+
def available_options(key=nil)
|
37
|
+
key.nil? ? enums : enums[key.to_sym]
|
38
|
+
end
|
39
|
+
|
40
|
+
# Returns whether single keyword valid
|
41
|
+
# TODO: Allow passing of key or array of keys
|
42
|
+
def valid_option?(key)
|
43
|
+
key = key.to_s.gsub(/\=$/,'').to_sym #strip setter (=) from key, so can just check options hash
|
44
|
+
available_options.keys.include?(key)
|
45
|
+
end
|
46
|
+
|
47
|
+
def defaults
|
48
|
+
{}
|
49
|
+
end
|
50
|
+
|
51
|
+
# # Maybe too complicated
|
52
|
+
# def self.validator_klass
|
53
|
+
# class_eval <<-STR
|
54
|
+
# class MyValidator < ActiveModel::Validator
|
55
|
+
# validates :display, :presence => true
|
56
|
+
# end
|
57
|
+
# STR
|
58
|
+
# end
|
59
|
+
|
60
|
+
# def validator
|
61
|
+
# self.class.validator.new
|
62
|
+
# end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module SettingCrazy
|
2
|
+
module Template
|
3
|
+
|
4
|
+
class Enum < Hash
|
5
|
+
attr_reader :name, :options
|
6
|
+
|
7
|
+
def initialize(name, options, &block)
|
8
|
+
super()
|
9
|
+
@name = name
|
10
|
+
@options = options
|
11
|
+
instance_eval(&block)
|
12
|
+
end
|
13
|
+
|
14
|
+
def value(enum_value, description = enum_value)
|
15
|
+
self[description] = enum_value
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/settingcrazy/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Dan Draper"]
|
6
|
+
gem.email = ["daniel@codefire.com"]
|
7
|
+
gem.description = %q{An advanced setting manager for ActiveRecord models}
|
8
|
+
gem.summary = %q{An advanced setting manager for ActiveRecord models}
|
9
|
+
gem.homepage = ""
|
10
|
+
|
11
|
+
gem.add_development_dependency "rspec", "~> 2.11.0"
|
12
|
+
gem.add_development_dependency "activerecord", "~> 3"
|
13
|
+
gem.add_development_dependency "sqlite3"
|
14
|
+
|
15
|
+
gem.files = `git ls-files`.split($\)
|
16
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
17
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
18
|
+
gem.name = "settingcrazy"
|
19
|
+
gem.require_paths = ["lib"]
|
20
|
+
gem.version = SettingCrazy::VERSION
|
21
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe SettingCrazy do
|
4
|
+
describe "settings from parent" do
|
5
|
+
|
6
|
+
context "when parent present" do
|
7
|
+
let!(:farm) { Farm.create(:name => "Old MacDonald's") }
|
8
|
+
let!(:duck) { farm.ducks.create(:name => "Drake", :quacks => 10) }
|
9
|
+
subject { duck.settings }
|
10
|
+
|
11
|
+
before do
|
12
|
+
farm.settings.color = "brown"
|
13
|
+
farm.save!
|
14
|
+
end
|
15
|
+
|
16
|
+
its(:color) { should == "brown" }
|
17
|
+
end
|
18
|
+
|
19
|
+
context "when there is no parent" do
|
20
|
+
let!(:duck) { Duck.create(:name => "Drake", :quacks => 10) }
|
21
|
+
subject { duck.settings }
|
22
|
+
its(:color) { should be(nil) }
|
23
|
+
end
|
24
|
+
|
25
|
+
context "from a specific namespace on the parent" do
|
26
|
+
let!(:scenario) { Scenario.create(:name => "Scen") }
|
27
|
+
let!(:campaign) { scenario.campaigns.create(:name => "Regular campaign") }
|
28
|
+
subject { campaign.settings }
|
29
|
+
|
30
|
+
before do
|
31
|
+
scenario.settings.yahoo.network = "don't care"
|
32
|
+
scenario.settings.google.network = "search"
|
33
|
+
scenario.save!
|
34
|
+
end
|
35
|
+
|
36
|
+
its(:network) { should == "search" }
|
37
|
+
end
|
38
|
+
|
39
|
+
context "from a specific namespace on the parent but set via a Proc" do
|
40
|
+
let!(:scenario) { Scenario.create(:name => "Scen") }
|
41
|
+
let!(:clever_campaign_g) { scenario.clever_campaigns.create(:name => "Clever campaign", :setting_namespace => "google") }
|
42
|
+
let!(:clever_campaign_y) { scenario.clever_campaigns.create(:name => "Clever campaign", :setting_namespace => "yahoo") }
|
43
|
+
|
44
|
+
before do
|
45
|
+
scenario.settings.yahoo.network = "something"
|
46
|
+
scenario.settings.google.network = "another thing"
|
47
|
+
scenario.save!
|
48
|
+
end
|
49
|
+
|
50
|
+
context "yahoo" do
|
51
|
+
subject { clever_campaign_y.settings }
|
52
|
+
its(:network) { should == "something" }
|
53
|
+
end
|
54
|
+
|
55
|
+
context "google" do
|
56
|
+
subject { clever_campaign_g.settings }
|
57
|
+
its(:network) { should == "another thing" }
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
context "when the parent has a template" do
|
63
|
+
subject { note.settings }
|
64
|
+
|
65
|
+
context "and the parent is present" do
|
66
|
+
let!(:_case) { Case.create(:name => "Snowtown") }
|
67
|
+
let!(:note) { _case.notes.create(:name => "The Murderer") }
|
68
|
+
its(:foo) { should == "1234" }
|
69
|
+
end
|
70
|
+
|
71
|
+
context "but the parent is missing" do
|
72
|
+
let!(:note) { Note.create(:name => "Orphaned") }
|
73
|
+
its(:foo) { should be(nil) }
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe VendorInstance do
|
4
|
+
let(:model) { VendorInstance.create(:name => "VI") }
|
5
|
+
subject { model.settings }
|
6
|
+
|
7
|
+
context "settings direct assignment" do
|
8
|
+
before do
|
9
|
+
model.settings = {
|
10
|
+
:foo => "1234",
|
11
|
+
:bar => "abcd"
|
12
|
+
}
|
13
|
+
model.save!
|
14
|
+
end
|
15
|
+
|
16
|
+
its(:foo) { should == "1234" }
|
17
|
+
its(:bar) { should == "abcd" }
|
18
|
+
end
|
19
|
+
|
20
|
+
context "mass assignment" do
|
21
|
+
before do
|
22
|
+
model.attributes = {
|
23
|
+
:settings => {
|
24
|
+
:foo => "some value",
|
25
|
+
:wee => "another",
|
26
|
+
}
|
27
|
+
}
|
28
|
+
model.save!
|
29
|
+
end
|
30
|
+
|
31
|
+
its(:foo) { should == "some value" }
|
32
|
+
its(:wee) { should == "another" }
|
33
|
+
end
|
34
|
+
|
35
|
+
describe "setting namespaces" do
|
36
|
+
let(:model) { Scenario.create(:name => "Scenario") }
|
37
|
+
subject { model.settings.google }
|
38
|
+
|
39
|
+
context "direct assignment" do
|
40
|
+
before do
|
41
|
+
model.settings.google = {
|
42
|
+
:foo => "1234",
|
43
|
+
:bar => "abcd"
|
44
|
+
}
|
45
|
+
model.save!
|
46
|
+
end
|
47
|
+
|
48
|
+
its(:foo) { should == "1234" }
|
49
|
+
its(:bar) { should == "abcd" }
|
50
|
+
end
|
51
|
+
|
52
|
+
context "mass assignment" do
|
53
|
+
before do
|
54
|
+
model.attributes = {
|
55
|
+
:settings => {
|
56
|
+
:google => {
|
57
|
+
:foo => "some value",
|
58
|
+
:wee => "another",
|
59
|
+
}
|
60
|
+
}
|
61
|
+
}
|
62
|
+
model.save!
|
63
|
+
end
|
64
|
+
|
65
|
+
its(:foo) { should == "some value" }
|
66
|
+
its(:wee) { should == "another" }
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|