ledermann-rails-settings 1.2.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,5 +1,14 @@
1
- require 'rails-settings/version'
2
- require 'rails-settings/null_store'
3
- require 'rails-settings/active_record'
4
- require 'rails-settings/settings'
5
- require 'rails-settings/scoped_settings'
1
+ require 'rails-settings/setting_object'
2
+ require 'rails-settings/configuration'
3
+ require 'rails-settings/base'
4
+ require 'rails-settings/scopes'
5
+
6
+ ActiveRecord::Base.class_eval do
7
+ def self.has_settings(*args, &block)
8
+ RailsSettings::Configuration.new(*args.unshift(self), &block)
9
+
10
+ include RailsSettings::Base unless self.include?(RailsSettings::Base)
11
+ include RailsSettings::Scopes unless self.include?(RailsSettings::Scopes)
12
+ end
13
+ end
14
+
@@ -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.0'
3
- end
2
+ VERSION = '2.0.0'
3
+ end
@@ -1,28 +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
- # specify any dependencies here; for example:
22
- s.add_development_dependency "rake"
23
- s.add_development_dependency "sqlite3"
24
- # s.add_runtime_dependency "rest-client"
25
- s.add_dependency 'activerecord', '>= 2.3'
26
- s.add_development_dependency 'rake'
27
- 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'
28
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