Skip to content

Commit 77ffcae

Browse files
authored
Merge pull request #87 from Jesus/master
Allow using different classes for each setting
2 parents 659160e + 29c1ddc commit 77ffcae

File tree

8 files changed

+154
-52
lines changed

8 files changed

+154
-52
lines changed

README.md

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,6 @@ end
5252
Every setting is handled by the class `RailsSettings::SettingObject`. You can use your own class, e.g. for validations:
5353

5454
```ruby
55-
class Project < ActiveRecord::Base
56-
has_settings :info, :class_name => 'ProjectSettingObject'
57-
end
58-
5955
class ProjectSettingObject < RailsSettings::SettingObject
6056
validate do
6157
unless self.owner_name.present? && self.owner_name.is_a?(String)
@@ -65,6 +61,25 @@ class ProjectSettingObject < RailsSettings::SettingObject
6561
end
6662
```
6763

64+
Then you can use it like this:
65+
66+
```ruby
67+
class Project < ActiveRecord::Base
68+
has_settings :info, :class_name => 'ProjectSettingObject'
69+
end
70+
```
71+
72+
Or use it only on some of the settings:
73+
74+
```ruby
75+
class Project < ActiveRecord::Base
76+
has_settings do |s|
77+
s.key :calendar # Will use the default RailsSettings::SettingObject
78+
s.key :info, :class_name => 'ProjectSettingObject'
79+
end
80+
end
81+
```
82+
6883
### Set settings
6984

7085
```ruby

lib/rails-settings.rb

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,15 @@ module RailsSettings
55
def self.can_protect_attributes?
66
(ActiveRecord::VERSION::MAJOR == 3) || defined?(ProtectedAttributes)
77
end
8+
9+
def self.can_use_becomes?
10+
![
11+
[3, 0],
12+
[3, 1],
13+
[3, 2],
14+
[4, 0]
15+
].include?([ActiveRecord::VERSION::MAJOR, ActiveRecord::VERSION::MINOR])
16+
end
817
end
918

1019
require 'rails-settings/setting_object'
@@ -20,4 +29,3 @@ def self.has_settings(*args, &block)
2029
extend RailsSettings::Scopes
2130
end
2231
end
23-

lib/rails-settings/base.rb

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,14 @@ def self.included(base)
66
:as => :target,
77
:autosave => true,
88
:dependent => :delete_all,
9-
:class_name => self.setting_object_class_name
9+
:class_name => "RailsSettings::SettingObject"
1010

1111
def settings(var)
1212
raise ArgumentError unless var.is_a?(Symbol)
13-
raise ArgumentError.new("Unknown key: #{var}") unless self.class.default_settings[var]
13+
raise ArgumentError.new("Unknown key: #{var}") unless self.class.setting_keys[var]
1414

15-
if RailsSettings.can_protect_attributes?
16-
setting_objects.detect { |s| s.var == var.to_s } || setting_objects.build({ :var => var.to_s }, :without_protection => true)
17-
else
18-
setting_objects.detect { |s| s.var == var.to_s } || setting_objects.build(:var => var.to_s, :target => self)
19-
end
15+
fetch_settings_record(var)
16+
.becomes(self.class.setting_keys[var][:class_name].constantize)
2017
end
2118

2219
def settings=(value)
@@ -36,11 +33,27 @@ def settings?(var=nil)
3633
end
3734

3835
def to_settings_hash
39-
settings_hash = self.class.default_settings.dup
40-
settings_hash.each do |var, vals|
41-
settings_hash[var] = settings_hash[var].merge(settings(var.to_sym).value)
36+
Hash[self.class.setting_keys.map do |key, options|
37+
[key, options[:default_value].merge(settings(key.to_sym).value)]
38+
end]
39+
end
40+
41+
private
42+
43+
def fetch_settings_record(var)
44+
find_settings_record(var) or build_settings_record(var)
45+
end
46+
47+
def find_settings_record(var)
48+
setting_objects.detect { |s| s.var == var.to_s }
49+
end
50+
51+
def build_settings_record(var)
52+
if RailsSettings.can_protect_attributes?
53+
setting_objects.build({ :var => var.to_s }, :without_protection => true)
54+
else
55+
setting_objects.build(:var => var.to_s, :target => self)
4256
end
43-
settings_hash
4457
end
4558
end
4659
end

lib/rails-settings/configuration.rb

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,53 @@
11
module RailsSettings
22
class Configuration
33
def initialize(*args, &block)
4-
options = args.extract_options!
4+
@default_options = args.extract_options!
5+
validate_options @default_options
56
klass = args.shift
67
keys = args
78

89
raise ArgumentError unless klass
910

1011
@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'
12+
@klass.class_attribute :setting_keys
13+
@klass.setting_keys = {}
1414

1515
if block_given?
1616
yield(self)
1717
else
18-
keys.each do |k|
19-
key(k)
20-
end
18+
keys.each { |k| key(k) }
2119
end
2220

23-
raise ArgumentError.new('has_settings: No keys defined') if @klass.default_settings.blank?
21+
raise ArgumentError.new('has_settings: No keys defined') if @klass.setting_keys.empty?
2422
end
2523

2624
def key(name, options={})
25+
validate_name name
26+
validate_options options
27+
options = @default_options.merge(options)
28+
29+
@klass.setting_keys[name] = {
30+
:default_value => (options[:defaults] || {}).stringify_keys.freeze,
31+
:class_name => (options[:class_name] || 'RailsSettings::SettingObject')
32+
}
33+
end
34+
35+
private
36+
37+
def validate_name(name)
2738
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
39+
end
40+
41+
def validate_options(options)
42+
valid_options = [:defaults, :class_name]
43+
options.each do |key, value|
44+
unless valid_options.include?(key)
45+
raise ArgumentError.new("has_settings: Invalid option #{key}")
46+
end
47+
end
48+
if options[:class_name] && !options[:class_name].constantize.ancestors.include?(RailsSettings::SettingObject)
49+
raise ArgumentError.new("has_settings: #{options[:class_name]} must be a subclass of RailsSettings::SettingObject")
50+
end
3051
end
3152
end
3253
end

lib/rails-settings/setting_object.rb

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ class SettingObject < ActiveRecord::Base
88
validate do
99
errors.add(:value, "Invalid setting value") unless value.is_a? Hash
1010

11-
unless _target_class.default_settings[var.to_sym]
11+
unless _target_class.setting_keys[var.to_sym]
1212
errors.add(:var, "#{var} is not defined!")
1313
end
1414
end
@@ -28,6 +28,15 @@ def respond_to?(method_name, include_priv=false)
2828
super || method_name.to_s =~ REGEX_SETTER || _setting?(method_name)
2929
end
3030

31+
# Annoying hack for old Rails
32+
unless RailsSettings.can_use_becomes?
33+
def becomes(klass)
34+
became = super(klass)
35+
became.instance_variable_set("@changed_attributes", @changed_attributes)
36+
became
37+
end
38+
end
39+
3140
def method_missing(method_name, *args, &block)
3241
if block_given?
3342
super
@@ -55,7 +64,7 @@ def sanitize_for_mass_assignment(attributes, role = nil)
5564
private
5665
def _get_value(name)
5766
if value[name].nil?
58-
_target_class.default_settings[var.to_sym][name]
67+
_target_class.setting_keys[var.to_sym][:default_value][name]
5968
else
6069
value[name]
6170
end
@@ -78,7 +87,12 @@ def _target_class
7887
end
7988

8089
def _setting?(method_name)
81-
_target_class.default_settings[var.to_sym].keys.include?(method_name.to_s)
90+
return false if target_id.nil? || target_type.nil?
91+
92+
_target_class
93+
.setting_keys[var.to_sym][:default_value]
94+
.keys
95+
.include?(method_name.to_s)
8296
end
8397
end
8498
end

spec/configuration_spec.rb

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,28 +8,32 @@ class Dummy
88
it "should define single key" do
99
Configuration.new(Dummy, :dashboard)
1010

11-
expect(Dummy.default_settings).to eq({ :dashboard => {} })
12-
expect(Dummy.setting_object_class_name).to eq('RailsSettings::SettingObject')
11+
expect(Dummy.setting_keys[:dashboard][:default_value]).to eq({})
12+
expect(Dummy.setting_keys[:dashboard][:class_name]).to eq('RailsSettings::SettingObject')
1313
end
1414

1515
it "should define multiple keys" do
1616
Configuration.new(Dummy, :dashboard, :calendar)
1717

18-
expect(Dummy.default_settings).to eq({ :dashboard => {}, :calendar => {} })
19-
expect(Dummy.setting_object_class_name).to eq('RailsSettings::SettingObject')
18+
expect(Dummy.setting_keys[:dashboard][:default_value]).to eq({})
19+
expect(Dummy.setting_keys[:calendar][:default_value]).to eq({})
20+
expect(Dummy.setting_keys[:dashboard][:class_name]).to eq('RailsSettings::SettingObject')
21+
expect(Dummy.setting_keys[:calendar][:class_name]).to eq('RailsSettings::SettingObject')
2022
end
2123

2224
it "should define single key with class_name" do
23-
Configuration.new(Dummy, :dashboard, :class_name => 'MyClass')
24-
expect(Dummy.default_settings).to eq({ :dashboard => {} })
25-
expect(Dummy.setting_object_class_name).to eq('MyClass')
25+
Configuration.new(Dummy, :dashboard, :class_name => 'ProjectSettingObject')
26+
expect(Dummy.setting_keys[:dashboard][:default_value]).to eq({})
27+
expect(Dummy.setting_keys[:dashboard][:class_name]).to eq('ProjectSettingObject')
2628
end
2729

2830
it "should define multiple keys with class_name" do
29-
Configuration.new(Dummy, :dashboard, :calendar, :class_name => 'MyClass')
31+
Configuration.new(Dummy, :dashboard, :calendar, :class_name => 'ProjectSettingObject')
3032

31-
expect(Dummy.default_settings).to eq({ :dashboard => {}, :calendar => {} })
32-
expect(Dummy.setting_object_class_name).to eq('MyClass')
33+
expect(Dummy.setting_keys[:dashboard][:default_value]).to eq({})
34+
expect(Dummy.setting_keys[:calendar][:default_value]).to eq({})
35+
expect(Dummy.setting_keys[:dashboard][:class_name]).to eq('ProjectSettingObject')
36+
expect(Dummy.setting_keys[:calendar][:class_name]).to eq('ProjectSettingObject')
3337
end
3438

3539
it "should define using block" do
@@ -38,8 +42,10 @@ class Dummy
3842
c.key :calendar
3943
end
4044

41-
expect(Dummy.default_settings).to eq({ :dashboard => {}, :calendar => {} })
42-
expect(Dummy.setting_object_class_name).to eq('RailsSettings::SettingObject')
45+
expect(Dummy.setting_keys[:dashboard][:default_value]).to eq({})
46+
expect(Dummy.setting_keys[:calendar][:default_value]).to eq({})
47+
expect(Dummy.setting_keys[:dashboard][:class_name]).to eq('RailsSettings::SettingObject')
48+
expect(Dummy.setting_keys[:calendar][:class_name]).to eq('RailsSettings::SettingObject')
4349
end
4450

4551
it "should define using block with defaults" do
@@ -48,18 +54,22 @@ class Dummy
4854
c.key :calendar, :defaults => { :scope => 'all' }
4955
end
5056

51-
expect(Dummy.default_settings).to eq({ :dashboard => { 'theme' => 'red' }, :calendar => { 'scope' => 'all'} })
52-
expect(Dummy.setting_object_class_name).to eq('RailsSettings::SettingObject')
57+
expect(Dummy.setting_keys[:dashboard][:default_value]).to eq({ 'theme' => 'red' })
58+
expect(Dummy.setting_keys[:calendar][:default_value]).to eq({ 'scope' => 'all'})
59+
expect(Dummy.setting_keys[:dashboard][:class_name]).to eq('RailsSettings::SettingObject')
60+
expect(Dummy.setting_keys[:calendar][:class_name]).to eq('RailsSettings::SettingObject')
5361
end
5462

5563
it "should define using block and class_name" do
56-
Configuration.new(Dummy, :class_name => 'MyClass') do |c|
64+
Configuration.new(Dummy, :class_name => 'ProjectSettingObject') do |c|
5765
c.key :dashboard
5866
c.key :calendar
5967
end
6068

61-
expect(Dummy.default_settings).to eq({ :dashboard => {}, :calendar => {} })
62-
expect(Dummy.setting_object_class_name).to eq('MyClass')
69+
expect(Dummy.setting_keys[:dashboard][:default_value]).to eq({})
70+
expect(Dummy.setting_keys[:calendar][:default_value]).to eq({})
71+
expect(Dummy.setting_keys[:dashboard][:class_name]).to eq('ProjectSettingObject')
72+
expect(Dummy.setting_keys[:calendar][:class_name]).to eq('ProjectSettingObject')
6373
end
6474
end
6575

@@ -104,5 +114,13 @@ class Dummy
104114
end
105115
}.to raise_error(ArgumentError)
106116
end
117+
118+
it "should fail with an invalid settings object" do
119+
expect {
120+
Configuration.new(Dummy) do |c|
121+
c.key :dashboard, :class_name => "InvalidSettingObject"
122+
end
123+
}.to raise_error(ArgumentError)
124+
end
107125
end
108126
end

spec/settings_spec.rb

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,16 @@
22

33
describe "Defaults" do
44
it "should be stored for simple class" do
5-
expect(Account.default_settings).to eq(:portal => {})
5+
expect(Account.setting_keys[:portal][:default_value]).to eq({})
66
end
77

88
it "should be stored for parent class" do
9-
expect(User.default_settings).to eq(:dashboard => { 'theme' => 'blue', 'view' => 'monthly', 'filter' => true },
10-
:calendar => { 'scope' => 'company'})
9+
expect(User.setting_keys[:dashboard][:default_value]).to eq({ 'theme' => 'blue', 'view' => 'monthly', 'filter' => true })
10+
expect(User.setting_keys[:calendar][:default_value]).to eq({ 'scope' => 'company'})
1111
end
1212

1313
it "should be stored for child class" do
14-
expect(GuestUser.default_settings).to eq(:dashboard => { 'theme' => 'red', 'view' => 'monthly', 'filter' => true })
14+
expect(GuestUser.setting_keys[:dashboard][:default_value]).to eq({ 'theme' => 'red', 'view' => 'monthly', 'filter' => true })
1515
end
1616
end
1717

@@ -32,6 +32,16 @@
3232
end
3333

3434
describe 'Objects' do
35+
context "settings should be an instance of :class_name" do
36+
it "should be an instance of 'SettingObject' by default" do
37+
expect(User.new.settings(:dashboard)).to be_a(RailsSettings::SettingObject)
38+
end
39+
40+
it "should be an instance of 'ProjectSettingObject' if defined" do
41+
expect(Project.new.settings(:info)).to be_a(ProjectSettingObject)
42+
end
43+
end
44+
3545
context 'without defaults' do
3646
let(:account) { Account.new :subdomain => 'foo' }
3747

spec/spec_helper.rb

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,7 @@ class Account < ActiveRecord::Base
6060
has_settings :portal
6161
end
6262

63-
class Project < ActiveRecord::Base
64-
has_settings :info, :class_name => 'ProjectSettingObject'
63+
class InvalidSettingObject
6564
end
6665

6766
class ProjectSettingObject < RailsSettings::SettingObject
@@ -72,6 +71,10 @@ class ProjectSettingObject < RailsSettings::SettingObject
7271
end
7372
end
7473

74+
class Project < ActiveRecord::Base
75+
has_settings :info, :class_name => 'ProjectSettingObject'
76+
end
77+
7578
def setup_db
7679
ActiveRecord::Base.configurations = YAML.load_file(File.dirname(__FILE__) + '/database.yml')
7780
ActiveRecord::Base.establish_connection(:sqlite)

0 commit comments

Comments
 (0)