settingcrazy 0.0.2
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/.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
|