ledermann-rails-settings 1.2.1 → 2.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.
@@ -0,0 +1,36 @@
1
+ module RailsSettings
2
+ module Base
3
+ def self.included(base)
4
+ base.class_eval do
5
+ has_many :setting_objects,
6
+ :as => :target,
7
+ :autosave => true,
8
+ :dependent => :delete_all,
9
+ :class_name => self.setting_object_class_name
10
+
11
+ def settings(var)
12
+ raise ArgumentError unless var.is_a?(Symbol)
13
+ raise ArgumentError.new("Unknown key: #{var}") unless self.class.default_settings[var]
14
+
15
+ setting_objects.detect { |s| s.var == var.to_s } || setting_objects.build(:var => var.to_s)
16
+ end
17
+
18
+ def settings=(value)
19
+ if value.nil?
20
+ setting_objects.each(&:mark_for_destruction)
21
+ else
22
+ raise ArgumentError
23
+ end
24
+ end
25
+
26
+ def settings?(var=nil)
27
+ if var.nil?
28
+ setting_objects.any? { |setting_object| !setting_object.marked_for_destruction? && setting_object.value.present? }
29
+ else
30
+ settings(var).value.present?
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,32 @@
1
+ module RailsSettings
2
+ class Configuration
3
+ def initialize(*args, &block)
4
+ options = args.extract_options!
5
+ klass = args.shift
6
+ keys = args
7
+
8
+ raise ArgumentError unless klass
9
+
10
+ @klass = klass
11
+ @klass.class_attribute :default_settings, :setting_object_class_name
12
+ @klass.default_settings = {}
13
+ @klass.setting_object_class_name = options[:class_name] || 'RailsSettings::SettingObject'
14
+
15
+ if block_given?
16
+ yield(self)
17
+ else
18
+ keys.each do |k|
19
+ key(k)
20
+ end
21
+ end
22
+
23
+ raise ArgumentError.new('has_settings: No keys defined') if @klass.default_settings.blank?
24
+ end
25
+
26
+ def key(name, options={})
27
+ raise ArgumentError.new("has_settings: Symbol expected, but got a #{name.class}") unless name.is_a?(Symbol)
28
+ raise ArgumentError.new("has_settings: Option :defaults expected, but got #{options.keys.join(', ')}") unless options.blank? || (options.keys == [:defaults])
29
+ @klass.default_settings[name] = (options[:defaults] || {}).stringify_keys.freeze
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,33 @@
1
+ module RailsSettings
2
+ module Scopes
3
+ def self.included(base)
4
+ base.class_eval do
5
+ scope :with_settings, lambda {
6
+ joins("INNER JOIN settings ON #{settings_join_condition}").
7
+ uniq
8
+ }
9
+
10
+ scope :with_settings_for, lambda { |var|
11
+ raise ArgumentError unless var.is_a?(Symbol)
12
+ joins("INNER JOIN settings ON #{settings_join_condition} AND settings.var = '#{var}'")
13
+ }
14
+
15
+ scope :without_settings, lambda {
16
+ joins("LEFT JOIN settings ON #{settings_join_condition}").
17
+ where('settings.id IS NULL')
18
+ }
19
+
20
+ scope :without_settings_for, lambda { |var|
21
+ raise ArgumentError unless var.is_a?(Symbol)
22
+ joins("LEFT JOIN settings ON #{settings_join_condition} AND settings.var = '#{var}'").
23
+ where('settings.id IS NULL')
24
+ }
25
+
26
+ def self.settings_join_condition
27
+ "settings.target_id = #{table_name}.#{primary_key} AND
28
+ settings.target_type = '#{base_class.name}'"
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,64 @@
1
+ module RailsSettings
2
+ class SettingObject < ActiveRecord::Base
3
+ self.table_name = 'settings'
4
+
5
+ belongs_to :target, :polymorphic => true
6
+
7
+ validates_presence_of :var, :value, :target_type
8
+ validate do
9
+ unless _target_class.default_settings[var.to_sym]
10
+ errors.add(:var, "#{var} is not defined!")
11
+ end
12
+ end
13
+
14
+ serialize :value, Hash
15
+
16
+ REGEX_SETTER = /\A([a-z]\w+)=\Z/i
17
+ REGEX_GETTER = /\A([a-z]\w+)\Z/i
18
+
19
+ def respond_to?(method_name, include_priv=false)
20
+ super || method_name.to_s =~ REGEX_SETTER
21
+ end
22
+
23
+ def method_missing(method_name, *args, &block)
24
+ if block_given?
25
+ super
26
+ else
27
+ if method_name.to_s =~ REGEX_SETTER && args.size == 1
28
+ _set_value($1, args.first)
29
+ elsif method_name.to_s =~ REGEX_GETTER && args.size == 0
30
+ _get_value($1)
31
+ else
32
+ super
33
+ end
34
+ end
35
+ end
36
+
37
+ private
38
+ def _get_value(name)
39
+ value[name] || _target_class.default_settings[var.to_sym][name]
40
+ end
41
+
42
+ def _set_value(name, v)
43
+ if value[name] != v
44
+ value_will_change!
45
+
46
+ if v.nil?
47
+ value.delete(name)
48
+ else
49
+ value[name] = v
50
+ end
51
+ end
52
+ end
53
+
54
+ def _target_class
55
+ target_type.constantize
56
+ end
57
+
58
+ def update(*)
59
+ # Patch ActiveRecord to save serialized attributes only if they are changed
60
+ # https://github.com/rails/rails/blob/3-2-stable/activerecord/lib/active_record/attribute_methods/dirty.rb#L70
61
+ super(changed) if changed?
62
+ end
63
+ end
64
+ end
@@ -1,3 +1,3 @@
1
1
  module RailsSettings
2
- VERSION = '1.2.1'
3
- end
2
+ VERSION = '2.0.0'
3
+ end
@@ -1,27 +1,25 @@
1
1
  # -*- encoding: utf-8 -*-
2
- $:.push File.expand_path('../lib', __FILE__)
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
4
  require 'rails-settings/version'
4
5
 
5
- Gem::Specification.new do |s|
6
- s.name = 'ledermann-rails-settings'
7
- s.version = RailsSettings::VERSION
8
- s.authors = ['Georg Ledermann']
9
- s.email = ['mail@georg-ledermann.de']
10
- s.homepage = 'https://github.com/ledermann/rails-settings'
11
- s.summary = %q{Settings management for ActiveRecord objects}
12
- s.description = %q{Ruby Gem that makes managing a table of key/value pairs easy. Think of it like a Hash stored in you database, that uses simple ActiveRecord like methods for manipulation.}
6
+ Gem::Specification.new do |gem|
7
+ gem.name = 'ledermann-rails-settings'
8
+ gem.version = RailsSettings::VERSION
9
+ gem.authors = ['Georg Ledermann']
10
+ gem.email = ['mail@georg-ledermann.de']
11
+ gem.description = %q{Settings gem for Ruby on Rails}
12
+ gem.summary = %q{Handling settings for ActiveRecord objects by storing them as serialized Hash in a separate database table. Optional: Defaults and Namespaces.}
13
+ gem.homepage = 'https://github.com/ledermann/rails-settings'
13
14
 
14
- s.rubyforge_project = 'ledermann-rails-settings'
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.require_paths = ['lib']
15
19
 
16
- s.files = `git ls-files`.split("\n")
17
- s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
- s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
- s.require_paths = ['lib']
20
+ gem.add_dependency 'activerecord', '~> 3.1'
20
21
 
21
- s.add_dependency 'activerecord', '>= 2.3'
22
-
23
- s.add_development_dependency 'rake'
24
- s.add_development_dependency 'sqlite3'
25
- s.add_development_dependency 'rake'
26
- s.add_development_dependency 'sqlite3'
22
+ gem.add_development_dependency 'rake', '~> 10.0'
23
+ gem.add_development_dependency 'sqlite3', '~> 1.3'
24
+ gem.add_development_dependency 'rspec', '~> 2.13'
27
25
  end
@@ -0,0 +1,108 @@
1
+ require 'spec_helper'
2
+
3
+ module RailsSettings
4
+ class Dummy
5
+ end
6
+
7
+ describe Configuration, 'successful' do
8
+ it "should define single key" do
9
+ Configuration.new(Dummy, :dashboard)
10
+
11
+ Dummy.default_settings.should == { :dashboard => {} }
12
+ Dummy.setting_object_class_name.should == 'RailsSettings::SettingObject'
13
+ end
14
+
15
+ it "should define multiple keys" do
16
+ Configuration.new(Dummy, :dashboard, :calendar)
17
+
18
+ Dummy.default_settings.should == { :dashboard => {}, :calendar => {} }
19
+ Dummy.setting_object_class_name.should == 'RailsSettings::SettingObject'
20
+ end
21
+
22
+ it "should define single key with class_name" do
23
+ Configuration.new(Dummy, :dashboard, :class_name => 'MyClass')
24
+ Dummy.default_settings.should == { :dashboard => {} }
25
+ Dummy.setting_object_class_name.should == 'MyClass'
26
+ end
27
+
28
+ it "should define multiple keys with class_name" do
29
+ Configuration.new(Dummy, :dashboard, :calendar, :class_name => 'MyClass')
30
+
31
+ Dummy.default_settings.should == { :dashboard => {}, :calendar => {} }
32
+ Dummy.setting_object_class_name.should == 'MyClass'
33
+ end
34
+
35
+ it "should define using block" do
36
+ Configuration.new(Dummy) do |c|
37
+ c.key :dashboard
38
+ c.key :calendar
39
+ end
40
+
41
+ Dummy.default_settings.should == { :dashboard => {}, :calendar => {} }
42
+ Dummy.setting_object_class_name.should == 'RailsSettings::SettingObject'
43
+ end
44
+
45
+ it "should define using block with defaults" do
46
+ Configuration.new(Dummy) do |c|
47
+ c.key :dashboard, :defaults => { :theme => 'red' }
48
+ c.key :calendar, :defaults => { :scope => 'all' }
49
+ end
50
+
51
+ Dummy.default_settings.should == { :dashboard => { 'theme' => 'red' }, :calendar => { 'scope' => 'all'} }
52
+ Dummy.setting_object_class_name.should == 'RailsSettings::SettingObject'
53
+ end
54
+
55
+ it "should define using block and class_name" do
56
+ Configuration.new(Dummy, :class_name => 'MyClass') do |c|
57
+ c.key :dashboard
58
+ c.key :calendar
59
+ end
60
+
61
+ Dummy.default_settings.should == { :dashboard => {}, :calendar => {} }
62
+ Dummy.setting_object_class_name.should == 'MyClass'
63
+ end
64
+ end
65
+
66
+ describe Configuration, 'failure' do
67
+ it "should fail without args" do
68
+ expect {
69
+ Configuration.new
70
+ }.to raise_error(ArgumentError)
71
+ end
72
+
73
+ it "should fail without keys" do
74
+ expect {
75
+ Configuration.new(Dummy)
76
+ }.to raise_error(ArgumentError)
77
+ end
78
+
79
+ it "should fail without keys in block" do
80
+ expect {
81
+ Configuration.new(Dummy) do |c|
82
+ end
83
+ }.to raise_error(ArgumentError)
84
+ end
85
+
86
+ it "should fail with keys not being symbols" do
87
+ expect {
88
+ Configuration.new(Dummy, 42, "string")
89
+ }.to raise_error(ArgumentError)
90
+ end
91
+
92
+ it "should fail with keys not being symbols" do
93
+ expect {
94
+ Configuration.new(Dummy) do |c|
95
+ c.key 42, "string"
96
+ end
97
+ }.to raise_error(ArgumentError)
98
+ end
99
+
100
+ it "should fail with unknown option" do
101
+ expect {
102
+ Configuration.new(Dummy) do |c|
103
+ c.key :dashboard, :foo => {}
104
+ end
105
+ }.to raise_error(ArgumentError)
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,108 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'Queries performed' do
4
+ context 'New record' do
5
+ let!(:user) { User.new :name => 'Mr. Pink' }
6
+
7
+ it 'should be saved by one SQL query' do
8
+ expect {
9
+ user.save!
10
+ }.to perform_queries(1)
11
+ end
12
+
13
+ it 'should be saved with settings for one key by two SQL queries' do
14
+ expect {
15
+ user.settings(:dashboard).foo = 42
16
+ user.settings(:dashboard).bar = 'string'
17
+ user.save!
18
+ }.to perform_queries(2)
19
+ end
20
+
21
+ it 'should be saved with settings for two keys by three SQL queries' do
22
+ expect {
23
+ user.settings(:dashboard).foo = 42
24
+ user.settings(:dashboard).bar = 'string'
25
+ user.settings(:calendar).bar = 'string'
26
+ user.save!
27
+ }.to perform_queries(3)
28
+ end
29
+ end
30
+
31
+ context 'Existing record without settings' do
32
+ let!(:user) { User.create! :name => 'Mr. Pink' }
33
+
34
+ it 'should be saved without SQL queries' do
35
+ expect {
36
+ user.save!
37
+ }.to perform_queries(0)
38
+ end
39
+
40
+ it 'should be saved with settings for one key by two SQL queries' do
41
+ expect {
42
+ user.settings(:dashboard).foo = 42
43
+ user.settings(:dashboard).bar = 'string'
44
+ user.save!
45
+ }.to perform_queries(2)
46
+ end
47
+
48
+ it 'should be saved with settings for two keys by three SQL queries' do
49
+ expect {
50
+ user.settings(:dashboard).foo = 42
51
+ user.settings(:dashboard).bar = 'string'
52
+ user.settings(:calendar).bar = 'string'
53
+ user.save!
54
+ }.to perform_queries(3)
55
+ end
56
+ end
57
+
58
+ context 'Existing record with settings' do
59
+ let!(:user) do
60
+ User.create! :name => 'Mr. Pink' do |user|
61
+ user.settings(:dashboard).theme = 'pink'
62
+ user.settings(:calendar).scope = 'all'
63
+ end
64
+ end
65
+
66
+ it 'should be saved without SQL queries' do
67
+ expect {
68
+ user.save!
69
+ }.to perform_queries(0)
70
+ end
71
+
72
+ it 'should be saved with settings for one key by one SQL queries' do
73
+ expect {
74
+ user.settings(:dashboard).foo = 42
75
+ user.settings(:dashboard).bar = 'string'
76
+ user.save!
77
+ }.to perform_queries(1)
78
+ end
79
+
80
+ it 'should be saved with settings for two keys by two SQL queries' do
81
+ expect {
82
+ user.settings(:dashboard).foo = 42
83
+ user.settings(:dashboard).bar = 'string'
84
+ user.settings(:calendar).bar = 'string'
85
+ user.save!
86
+ }.to perform_queries(2)
87
+ end
88
+
89
+ it 'should be destroyed by two SQL queries' do
90
+ expect {
91
+ user.destroy
92
+ }.to perform_queries(2)
93
+ end
94
+
95
+ it "should update settings by one SQL query" do
96
+ expect {
97
+ user.settings(:dashboard).update_attributes! :foo => 'bar'
98
+ }.to perform_queries(1)
99
+ end
100
+
101
+ it "should not touch database if there are no changes made" do
102
+ expect {
103
+ user.settings(:dashboard).update_attributes :theme => 'pink'
104
+ user.settings(:calendar).update_attributes :scope => 'all'
105
+ }.to perform_queries(0)
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,31 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'scopes' do
4
+ let!(:user1) { User.create! :name => 'Mr. White' do |user| user.settings(:dashboard).theme = 'white' end }
5
+ let!(:user2) { User.create! :name => 'Mr. Blue' }
6
+
7
+ it "should find objects with existing settings" do
8
+ User.with_settings.should eq([user1])
9
+ end
10
+
11
+ it "should find objects with settings for key" do
12
+ User.with_settings_for(:dashboard).should eq([user1])
13
+ User.with_settings_for(:foo).should eq([])
14
+ end
15
+
16
+ it "should records without settings" do
17
+ User.without_settings.should eq([user2])
18
+ end
19
+
20
+ it "should records without settings for key" do
21
+ User.without_settings_for(:foo).should eq([user1, user2])
22
+ User.without_settings_for(:dashboard).should eq([user2])
23
+ end
24
+
25
+ it "should require symbol as key" do
26
+ [ nil, "string", 42 ].each do |invalid_key|
27
+ expect { User.without_settings_for(invalid_key) }.to raise_error(ArgumentError)
28
+ expect { User.with_settings_for(invalid_key) }.to raise_error(ArgumentError)
29
+ end
30
+ end
31
+ end