settingcrazy 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. data/.gitignore +17 -0
  2. data/.rvmrc +1 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE +22 -0
  5. data/README.md +245 -0
  6. data/Rakefile +2 -0
  7. data/TODO +10 -0
  8. data/db/safeattributes.db +0 -0
  9. data/lib/generators/settingcrazy/setting_values_migration/setting_values_migration_generator.rb +18 -0
  10. data/lib/generators/settingcrazy/setting_values_migration/templates/migration.rb +15 -0
  11. data/lib/settingcrazy.rb +23 -0
  12. data/lib/settingcrazy/class_methods.rb +32 -0
  13. data/lib/settingcrazy/inheritor.rb +31 -0
  14. data/lib/settingcrazy/instance_methods.rb +20 -0
  15. data/lib/settingcrazy/namespace.rb +14 -0
  16. data/lib/settingcrazy/namespaced_settings_proxy.rb +18 -0
  17. data/lib/settingcrazy/setting_value.rb +11 -0
  18. data/lib/settingcrazy/settings_proxy.rb +107 -0
  19. data/lib/settingcrazy/settings_validator.rb +43 -0
  20. data/lib/settingcrazy/template.rb +2 -0
  21. data/lib/settingcrazy/template/base.rb +66 -0
  22. data/lib/settingcrazy/template/enum.rb +19 -0
  23. data/lib/settingcrazy/version.rb +3 -0
  24. data/settingcrazy.gemspec +21 -0
  25. data/spec/inheritance_spec.rb +76 -0
  26. data/spec/mass_assignment_spec.rb +69 -0
  27. data/spec/namespaced_settings_proxy_spec.rb +70 -0
  28. data/spec/settingcrazy_spec.rb +78 -0
  29. data/spec/settings_proxy_spec.rb +55 -0
  30. data/spec/settings_validator_spec.rb +121 -0
  31. data/spec/spec_helper.rb +8 -0
  32. data/spec/support/models.rb +10 -0
  33. data/spec/support/models/campaign.rb +12 -0
  34. data/spec/support/models/case.rb +11 -0
  35. data/spec/support/models/clever_campaign.rb +13 -0
  36. data/spec/support/models/duck.rb +13 -0
  37. data/spec/support/models/farm.rb +10 -0
  38. data/spec/support/models/note.rb +12 -0
  39. data/spec/support/models/scenario.rb +13 -0
  40. data/spec/support/models/setting_value.rb +8 -0
  41. data/spec/support/models/templated_campaign.rb +11 -0
  42. data/spec/support/models/templated_scenario.rb +11 -0
  43. data/spec/support/models/vendor_instance.rb +9 -0
  44. data/spec/support/templates.rb +3 -0
  45. data/spec/support/templates/example_campaign_template.rb +24 -0
  46. data/spec/support/templates/example_template.rb +12 -0
  47. metadata +161 -0
@@ -0,0 +1,14 @@
1
+ module SettingCrazy
2
+ class Namespace
3
+ attr_reader :template
4
+
5
+ def initialize(name, options = {})
6
+ @name = name.to_sym
7
+ @template = options[:template]
8
+ end
9
+
10
+ def name
11
+ @name.to_s
12
+ end
13
+ end
14
+ end
@@ -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,11 @@
1
+ module SettingCrazy
2
+ class SettingValue < ActiveRecord::Base
3
+ attr_accessible :key, :value, :namespace
4
+ serialize :value
5
+ belongs_to :settable, :polymorphic => true
6
+
7
+ def self.namespace(namespace)
8
+ where(:namespace => namespace)
9
+ end
10
+ end
11
+ 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,2 @@
1
+ require_relative 'template/base'
2
+ require_relative 'template/enum'
@@ -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,3 @@
1
+ module SettingCrazy
2
+ VERSION = "0.0.2"
3
+ 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