rails-properties 3.4.3

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,84 @@
1
+ module RailsProperties
2
+ class PropertyObject < ActiveRecord::Base
3
+ self.table_name = 'properties'
4
+
5
+ belongs_to :target, :polymorphic => true
6
+
7
+ validates_presence_of :var, :target_type
8
+ validate do
9
+ errors.add(:value, "Invalid property value") unless value.is_a? Hash
10
+
11
+ unless _target_class.default_properties[var.to_sym]
12
+ errors.add(:var, "#{var} is not defined!")
13
+ end
14
+ end
15
+
16
+ serialize :value, Hash
17
+
18
+ if RailsProperties.can_protect_attributes?
19
+ # attr_protected can not be used here because it touches the database which is not connected yet.
20
+ # So allow no attributes and override <tt>#sanitize_for_mass_assignment</tt>
21
+ attr_accessible
22
+ end
23
+
24
+ REGEX_SETTER = /\A([a-z]\w+)=\Z/i
25
+ REGEX_GETTER = /\A([a-z]\w+)\Z/i
26
+
27
+ def respond_to?(method_name, include_priv=false)
28
+ super || method_name.to_s =~ REGEX_SETTER || _property?(method_name)
29
+ end
30
+
31
+ def method_missing(method_name, *args, &block)
32
+ if block_given?
33
+ super
34
+ else
35
+ if attribute_names.include?(method_name.to_s.sub('=',''))
36
+ super
37
+ elsif method_name.to_s =~ REGEX_SETTER && args.size == 1
38
+ _set_value($1, args.first)
39
+ elsif method_name.to_s =~ REGEX_GETTER && args.size == 0
40
+ _get_value($1)
41
+ else
42
+ super
43
+ end
44
+ end
45
+ end
46
+
47
+ protected
48
+ if RailsProperties.can_protect_attributes?
49
+ # Simulate attr_protected by removing all regular attributes
50
+ def sanitize_for_mass_assignment(attributes, role = nil)
51
+ attributes.except('id', 'var', 'value', 'target_id', 'target_type', 'created_at', 'updated_at')
52
+ end
53
+ end
54
+
55
+ private
56
+ def _get_value(name)
57
+ if value[name].nil?
58
+ _target_class.default_properties[var.to_sym][name]
59
+ else
60
+ value[name]
61
+ end
62
+ end
63
+
64
+ def _set_value(name, v)
65
+ if value[name] != v
66
+ value_will_change!
67
+
68
+ if v.nil?
69
+ value.delete(name)
70
+ else
71
+ value[name] = v
72
+ end
73
+ end
74
+ end
75
+
76
+ def _target_class
77
+ target_type.constantize
78
+ end
79
+
80
+ def _property?(method_name)
81
+ _target_class.default_properties[var.to_sym].keys.include?(method_name.to_s)
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,34 @@
1
+ module RailsProperties
2
+ module Scopes
3
+ def with_properties
4
+ result = joins("INNER JOIN properties ON #{properties_join_condition}")
5
+
6
+ if ActiveRecord::VERSION::MAJOR < 5
7
+ result.uniq
8
+ else
9
+ result.distinct
10
+ end
11
+ end
12
+
13
+ def with_properties_for(var)
14
+ raise ArgumentError.new('Symbol expected!') unless var.is_a?(Symbol)
15
+ joins("INNER JOIN properties ON #{properties_join_condition} AND properties.var = '#{var}'")
16
+ end
17
+
18
+ def without_properties
19
+ joins("LEFT JOIN properties ON #{properties_join_condition}").
20
+ where('properties.id IS NULL')
21
+ end
22
+
23
+ def without_properties_for(var)
24
+ raise ArgumentError.new('Symbol expected!') unless var.is_a?(Symbol)
25
+ joins("LEFT JOIN properties ON #{properties_join_condition} AND properties.var = '#{var}'").
26
+ where('properties.id IS NULL')
27
+ end
28
+
29
+ def properties_join_condition
30
+ "properties.target_id = #{table_name}.#{primary_key} AND
31
+ properties.target_type = '#{base_class.name}'"
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,3 @@
1
+ module RailsProperties
2
+ VERSION = '3.4.3'
3
+ end
@@ -0,0 +1,29 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'rails-properties/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = 'rails-properties'
8
+ gem.version = RailsProperties::VERSION
9
+ gem.licenses = ['MIT']
10
+ gem.authors = ['Fletcher Fowler']
11
+ gem.email = ['fletch@fzf.me']
12
+ gem.description = %q{Properties gem for Ruby on Rails}
13
+ gem.summary = %q{Ruby gem to handle properties for ActiveRecord instances by storing them as serialized Hash in a separate database table. Namespaces and defaults included.}
14
+ gem.homepage = 'https://github.com/1debit/rails-properties'
15
+ gem.required_ruby_version = '>= 1.9.3'
16
+
17
+ gem.files = `git ls-files`.split($/)
18
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
19
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
20
+ gem.require_paths = ['lib']
21
+
22
+ gem.add_dependency 'activerecord', '>= 3.1'
23
+
24
+ gem.add_development_dependency 'rake'
25
+ gem.add_development_dependency 'sqlite3'
26
+ gem.add_development_dependency 'rspec'
27
+ gem.add_development_dependency 'coveralls'
28
+ gem.add_development_dependency 'simplecov', RUBY_VERSION < '2' ? '~> 0.11.2' : '>= 0.11.2'
29
+ end
@@ -0,0 +1,108 @@
1
+ require 'spec_helper'
2
+
3
+ module RailsProperties
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
+ expect(Dummy.default_properties).to eq({ :dashboard => {} })
12
+ expect(Dummy.property_object_class_name).to eq('RailsProperties::PropertyObject')
13
+ end
14
+
15
+ it "should define multiple keys" do
16
+ Configuration.new(Dummy, :dashboard, :calendar)
17
+
18
+ expect(Dummy.default_properties).to eq({ :dashboard => {}, :calendar => {} })
19
+ expect(Dummy.property_object_class_name).to eq('RailsProperties::PropertyObject')
20
+ end
21
+
22
+ it "should define single key with class_name" do
23
+ Configuration.new(Dummy, :dashboard, :class_name => 'MyClass')
24
+ expect(Dummy.default_properties).to eq({ :dashboard => {} })
25
+ expect(Dummy.property_object_class_name).to eq('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
+ expect(Dummy.default_properties).to eq({ :dashboard => {}, :calendar => {} })
32
+ expect(Dummy.property_object_class_name).to eq('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
+ expect(Dummy.default_properties).to eq({ :dashboard => {}, :calendar => {} })
42
+ expect(Dummy.property_object_class_name).to eq('RailsProperties::PropertyObject')
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
+ expect(Dummy.default_properties).to eq({ :dashboard => { 'theme' => 'red' }, :calendar => { 'scope' => 'all'} })
52
+ expect(Dummy.property_object_class_name).to eq('RailsProperties::PropertyObject')
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
+ expect(Dummy.default_properties).to eq({ :dashboard => {}, :calendar => {} })
62
+ expect(Dummy.property_object_class_name).to eq('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,3 @@
1
+ sqlite:
2
+ adapter: sqlite3
3
+ database: ":memory:"
@@ -0,0 +1,248 @@
1
+ require 'spec_helper'
2
+
3
+ describe "Defaults" do
4
+ it "should be stored for simple class" do
5
+ expect(Account.default_properties).to eq(:portal => {})
6
+ end
7
+
8
+ it "should be stored for parent class" do
9
+ expect(User.default_properties).to eq(:dashboard => { 'theme' => 'blue', 'view' => 'monthly', 'filter' => true },
10
+ :calendar => { 'scope' => 'company'})
11
+ end
12
+
13
+ it "should be stored for child class" do
14
+ expect(GuestUser.default_properties).to eq(:dashboard => { 'theme' => 'red', 'view' => 'monthly', 'filter' => true })
15
+ end
16
+ end
17
+
18
+ describe "Getter/Setter" do
19
+ let(:account) { Account.new :subdomain => 'foo' }
20
+
21
+ it "should handle method syntax" do
22
+ account.properties(:portal).enabled = true
23
+ account.properties(:portal).template = 'black'
24
+
25
+ expect(account.properties(:portal).enabled).to eq(true)
26
+ expect(account.properties(:portal).template).to eq('black')
27
+ end
28
+
29
+ it "should return nil for not existing key" do
30
+ expect(account.properties(:portal).foo).to eq(nil)
31
+ end
32
+ end
33
+
34
+ describe 'Objects' do
35
+ context 'without defaults' do
36
+ let(:account) { Account.new :subdomain => 'foo' }
37
+
38
+ it 'should have blank properties' do
39
+ expect(account.properties(:portal).value).to eq({})
40
+ end
41
+
42
+ it 'should allow saving a blank value' do
43
+ account.save!
44
+ expect(account.properties(:portal).save).to be_truthy
45
+ end
46
+
47
+ it 'should allow removing all values' do
48
+ account.properties(:portal).premium = true
49
+ account.properties(:portal).fee = 42.5
50
+ account.save!
51
+
52
+ account.properties(:portal).premium = nil
53
+ expect(account.save).to be_truthy
54
+
55
+ account.properties(:portal).fee = nil
56
+ expect(account.save).to be_truthy
57
+ end
58
+
59
+ it 'should not add properties on saving' do
60
+ account.save!
61
+ expect(RailsProperties::PropertyObject.count).to eq(0)
62
+ end
63
+
64
+ it "should save object with properties" do
65
+ account.properties(:portal).premium = true
66
+ account.properties(:portal).fee = 42.5
67
+ account.save!
68
+
69
+ account.reload
70
+ expect(account.properties(:portal).premium).to eq(true)
71
+ expect(account.properties(:portal).fee).to eq(42.5)
72
+
73
+ expect(RailsProperties::PropertyObject.count).to eq(1)
74
+ expect(RailsProperties::PropertyObject.first.value).to eq({ 'premium' => true, 'fee' => 42.5 })
75
+ end
76
+
77
+ it "should save properties separated" do
78
+ account.save!
79
+
80
+ properties = account.properties(:portal)
81
+ properties.enabled = true
82
+ properties.template = 'black'
83
+ properties.save!
84
+
85
+ account.reload
86
+ expect(account.properties(:portal).enabled).to eq(true)
87
+ expect(account.properties(:portal).template).to eq('black')
88
+ end
89
+ end
90
+
91
+ context 'with defaults' do
92
+ let(:user) { User.new :name => 'Mr. Brown' }
93
+
94
+ it 'should have default properties' do
95
+ expect(user.properties(:dashboard).theme).to eq('blue')
96
+ expect(user.properties(:dashboard).view).to eq('monthly')
97
+ expect(user.properties(:dashboard).filter).to eq(true)
98
+ expect(user.properties(:calendar).scope).to eq('company')
99
+ end
100
+
101
+ it 'should have default properties after changing one' do
102
+ user.properties(:dashboard).theme = 'gray'
103
+
104
+ expect(user.properties(:dashboard).theme).to eq('gray')
105
+ expect(user.properties(:dashboard).view).to eq('monthly')
106
+ expect(user.properties(:dashboard).filter).to eq(true)
107
+ expect(user.properties(:calendar).scope).to eq('company')
108
+ end
109
+
110
+ it "should overwrite properties" do
111
+ user.properties(:dashboard).theme = 'brown'
112
+ user.properties(:dashboard).filter = false
113
+ user.save!
114
+
115
+ user.reload
116
+ expect(user.properties(:dashboard).theme).to eq('brown')
117
+ expect(user.properties(:dashboard).filter).to eq(false)
118
+ expect(RailsProperties::PropertyObject.count).to eq(1)
119
+ expect(RailsProperties::PropertyObject.first.value).to eq({ 'theme' => 'brown', 'filter' => false })
120
+ end
121
+
122
+ it "should merge properties with defaults" do
123
+ user.properties(:dashboard).theme = 'brown'
124
+ user.save!
125
+
126
+ user.reload
127
+ expect(user.properties(:dashboard).theme).to eq('brown')
128
+ expect(user.properties(:dashboard).filter).to eq(true)
129
+ expect(RailsProperties::PropertyObject.count).to eq(1)
130
+ expect(RailsProperties::PropertyObject.first.value).to eq({ 'theme' => 'brown' })
131
+ end
132
+ end
133
+ end
134
+
135
+ describe "Object without properties" do
136
+ let!(:user) { User.create! :name => 'Mr. White' }
137
+
138
+ it "should respond to #properties?" do
139
+ expect(user.properties?).to eq(false)
140
+ expect(user.properties?(:dashboard)).to eq(false)
141
+ end
142
+
143
+ it "should have no property objects" do
144
+ expect(RailsProperties::PropertyObject.count).to eq(0)
145
+ end
146
+
147
+ it "should add properties" do
148
+ user.properties(:dashboard).update_attributes! :smart => true
149
+
150
+ user.reload
151
+ expect(user.properties(:dashboard).smart).to eq(true)
152
+ end
153
+
154
+ it "should not save properties if assigned nil" do
155
+ expect {
156
+ user.properties = nil
157
+ user.save!
158
+ }.to_not change(RailsProperties::PropertyObject, :count)
159
+ end
160
+ end
161
+
162
+ describe "Object with properties" do
163
+ let!(:user) do
164
+ User.create! :name => 'Mr. White' do |user|
165
+ user.properties(:dashboard).theme = 'white'
166
+ user.properties(:calendar).scope = 'all'
167
+ end
168
+ end
169
+
170
+ it "should respond to #properties?" do
171
+ expect(user.properties?).to eq(true)
172
+
173
+ expect(user.properties?(:dashboard)).to eq(true)
174
+ expect(user.properties?(:calendar)).to eq(true)
175
+ end
176
+
177
+ it "should have two property objects" do
178
+ expect(RailsProperties::PropertyObject.count).to eq(2)
179
+ end
180
+
181
+ it "should update properties" do
182
+ user.properties(:dashboard).update_attributes! :smart => true
183
+ user.reload
184
+
185
+ expect(user.properties(:dashboard).smart).to eq(true)
186
+ expect(user.properties(:dashboard).theme).to eq('white')
187
+ expect(user.properties(:calendar).scope).to eq('all')
188
+ end
189
+
190
+ it "should update properties by saving object" do
191
+ user.properties(:dashboard).smart = true
192
+ user.save!
193
+
194
+ user.reload
195
+ expect(user.properties(:dashboard).smart).to eq(true)
196
+ end
197
+
198
+ it "should destroy properties with nil" do
199
+ expect {
200
+ user.properties = nil
201
+ user.save!
202
+ }.to change(RailsProperties::PropertyObject, :count).by(-2)
203
+
204
+ expect(user.properties?).to eq(false)
205
+ end
206
+
207
+ it "should raise exception on assigning other than nil" do
208
+ expect {
209
+ user.properties = :foo
210
+ user.save!
211
+ }.to raise_error(ArgumentError)
212
+ end
213
+ end
214
+
215
+ describe "Customized PropertyObject" do
216
+ let(:project) { Project.create! :name => 'Heist' }
217
+
218
+ it "should not accept invalid attributes" do
219
+ project.properties(:info).owner_name = 42
220
+ expect(project.properties(:info)).not_to be_valid
221
+
222
+ project.properties(:info).owner_name = ''
223
+ expect(project.properties(:info)).not_to be_valid
224
+ end
225
+
226
+ it "should accept valid attributes" do
227
+ project.properties(:info).owner_name = 'Mr. Brown'
228
+ expect(project.properties(:info)).to be_valid
229
+ end
230
+ end
231
+
232
+ describe "to_properties_hash" do
233
+ let(:user) do
234
+ User.new :name => 'Mrs. Fin' do |user|
235
+ user.properties(:dashboard).theme = 'green'
236
+ user.properties(:dashboard).sound = 11
237
+ user.properties(:calendar).scope = 'some'
238
+ end
239
+ end
240
+
241
+ it "should return defaults" do
242
+ expect(User.new.to_properties_hash).to eq({:dashboard=>{"theme"=>"blue", "view"=>"monthly", "filter"=>true}, :calendar=>{"scope"=>"company"}})
243
+ end
244
+
245
+ it "should return merged properties" do
246
+ expect(user.to_properties_hash).to eq({:dashboard=>{"theme"=>"green", "view"=>"monthly", "filter"=>true, "sound" => 11}, :calendar=>{"scope"=>"some"}})
247
+ end
248
+ end