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.
- checksums.yaml +7 -0
- data/.travis.yml +2 -3
- data/Changelog.md +12 -1
- data/Gemfile +1 -1
- data/MIT-LICENSE +6 -5
- data/README.md +133 -153
- data/Rakefile +4 -9
- data/ci/Gemfile.rails-3.1.x +4 -3
- data/ci/Gemfile.rails-3.2.x +4 -3
- data/lib/generators/rails_settings/migration/migration_generator.rb +23 -0
- data/lib/generators/rails_settings/migration/templates/migration.rb +15 -0
- data/lib/rails-settings.rb +14 -4
- data/lib/rails-settings/base.rb +36 -0
- data/lib/rails-settings/configuration.rb +32 -0
- data/lib/rails-settings/scopes.rb +33 -0
- data/lib/rails-settings/setting_object.rb +64 -0
- data/lib/rails-settings/version.rb +2 -2
- data/rails-settings.gemspec +18 -20
- data/spec/configuration_spec.rb +108 -0
- data/spec/queries_spec.rb +108 -0
- data/spec/scopes_spec.rb +31 -0
- data/spec/serialize_spec.rb +40 -0
- data/spec/setting_object_spec.rb +119 -0
- data/spec/settings_spec.rb +206 -0
- data/spec/spec_helper.rb +90 -0
- data/spec/support/matchers/perform_queries.rb +18 -0
- data/spec/support/query_counter.rb +17 -0
- metadata +59 -72
- data/ci/Gemfile.rails-2.3.x +0 -5
- data/ci/Gemfile.rails-3.0.x +0 -5
- data/init.rb +0 -1
- data/lib/rails-settings/active_record.rb +0 -38
- data/lib/rails-settings/scoped_settings.rb +0 -20
- data/lib/rails-settings/settings.rb +0 -121
- data/test/settings_test.rb +0 -258
- data/test/test_helper.rb +0 -30
@@ -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 = '
|
3
|
-
end
|
2
|
+
VERSION = '2.0.0'
|
3
|
+
end
|
data/rails-settings.gemspec
CHANGED
@@ -1,27 +1,25 @@
|
|
1
1
|
# -*- encoding: utf-8 -*-
|
2
|
-
|
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 |
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
22
|
-
|
23
|
-
|
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
|
data/spec/scopes_spec.rb
ADDED
@@ -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
|