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.
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