HornsAndHooves-moribus 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +35 -0
  3. data/.rspec +4 -0
  4. data/.ruby-gemset +1 -0
  5. data/.ruby-version +1 -0
  6. data/.simplecov +42 -0
  7. data/.travis.yml +8 -0
  8. data/Gemfile +17 -0
  9. data/HornsAndHooves-moribus.gemspec +31 -0
  10. data/LICENSE +21 -0
  11. data/README.md +110 -0
  12. data/Rakefile +15 -0
  13. data/lib/colorized_text.rb +33 -0
  14. data/lib/moribus.rb +138 -0
  15. data/lib/moribus/aggregated_behavior.rb +80 -0
  16. data/lib/moribus/aggregated_cache_behavior.rb +76 -0
  17. data/lib/moribus/alias_association.rb +111 -0
  18. data/lib/moribus/extensions.rb +37 -0
  19. data/lib/moribus/extensions/delegate_associated.rb +48 -0
  20. data/lib/moribus/extensions/has_aggregated_extension.rb +94 -0
  21. data/lib/moribus/extensions/has_current_extension.rb +17 -0
  22. data/lib/moribus/macros.rb +135 -0
  23. data/lib/moribus/tracked_behavior.rb +91 -0
  24. data/lib/moribus/version.rb +3 -0
  25. data/spec/dummy/README.rdoc +261 -0
  26. data/spec/dummy/Rakefile +7 -0
  27. data/spec/dummy/app/assets/javascripts/application.js +15 -0
  28. data/spec/dummy/app/assets/stylesheets/application.css +13 -0
  29. data/spec/dummy/app/controllers/application_controller.rb +3 -0
  30. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  31. data/spec/dummy/app/mailers/.gitkeep +0 -0
  32. data/spec/dummy/app/models/.gitkeep +0 -0
  33. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  34. data/spec/dummy/config.ru +4 -0
  35. data/spec/dummy/config/application.rb +53 -0
  36. data/spec/dummy/config/boot.rb +10 -0
  37. data/spec/dummy/config/database.yml +25 -0
  38. data/spec/dummy/config/environment.rb +5 -0
  39. data/spec/dummy/config/environments/development.rb +31 -0
  40. data/spec/dummy/config/environments/production.rb +70 -0
  41. data/spec/dummy/config/environments/test.rb +34 -0
  42. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  43. data/spec/dummy/config/initializers/inflections.rb +15 -0
  44. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  45. data/spec/dummy/config/initializers/secret_token.rb +7 -0
  46. data/spec/dummy/config/initializers/session_store.rb +8 -0
  47. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  48. data/spec/dummy/config/locales/en.yml +5 -0
  49. data/spec/dummy/config/routes.rb +58 -0
  50. data/spec/dummy/db/test.sqlite3 +0 -0
  51. data/spec/dummy/lib/assets/.gitkeep +0 -0
  52. data/spec/dummy/log/.gitkeep +0 -0
  53. data/spec/dummy/public/404.html +26 -0
  54. data/spec/dummy/public/422.html +26 -0
  55. data/spec/dummy/public/500.html +25 -0
  56. data/spec/dummy/public/favicon.ico +0 -0
  57. data/spec/dummy/script/rails +6 -0
  58. data/spec/moribus/alias_association_spec.rb +88 -0
  59. data/spec/moribus/macros_spec.rb +7 -0
  60. data/spec/moribus_spec.rb +332 -0
  61. data/spec/spec_helper.rb +15 -0
  62. data/spec/support/moribus_spec_model.rb +57 -0
  63. metadata +247 -0
File without changes
File without changes
File without changes
@@ -0,0 +1,26 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>The page you were looking for doesn't exist (404)</title>
5
+ <style type="text/css">
6
+ body { background-color: #fff; color: #666; text-align: center; font-family: arial, sans-serif; }
7
+ div.dialog {
8
+ width: 25em;
9
+ padding: 0 4em;
10
+ margin: 4em auto 0 auto;
11
+ border: 1px solid #ccc;
12
+ border-right-color: #999;
13
+ border-bottom-color: #999;
14
+ }
15
+ h1 { font-size: 100%; color: #f00; line-height: 1.5em; }
16
+ </style>
17
+ </head>
18
+
19
+ <body>
20
+ <!-- This file lives in public/404.html -->
21
+ <div class="dialog">
22
+ <h1>The page you were looking for doesn't exist.</h1>
23
+ <p>You may have mistyped the address or the page may have moved.</p>
24
+ </div>
25
+ </body>
26
+ </html>
@@ -0,0 +1,26 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>The change you wanted was rejected (422)</title>
5
+ <style type="text/css">
6
+ body { background-color: #fff; color: #666; text-align: center; font-family: arial, sans-serif; }
7
+ div.dialog {
8
+ width: 25em;
9
+ padding: 0 4em;
10
+ margin: 4em auto 0 auto;
11
+ border: 1px solid #ccc;
12
+ border-right-color: #999;
13
+ border-bottom-color: #999;
14
+ }
15
+ h1 { font-size: 100%; color: #f00; line-height: 1.5em; }
16
+ </style>
17
+ </head>
18
+
19
+ <body>
20
+ <!-- This file lives in public/422.html -->
21
+ <div class="dialog">
22
+ <h1>The change you wanted was rejected.</h1>
23
+ <p>Maybe you tried to change something you didn't have access to.</p>
24
+ </div>
25
+ </body>
26
+ </html>
@@ -0,0 +1,25 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>We're sorry, but something went wrong (500)</title>
5
+ <style type="text/css">
6
+ body { background-color: #fff; color: #666; text-align: center; font-family: arial, sans-serif; }
7
+ div.dialog {
8
+ width: 25em;
9
+ padding: 0 4em;
10
+ margin: 4em auto 0 auto;
11
+ border: 1px solid #ccc;
12
+ border-right-color: #999;
13
+ border-bottom-color: #999;
14
+ }
15
+ h1 { font-size: 100%; color: #f00; line-height: 1.5em; }
16
+ </style>
17
+ </head>
18
+
19
+ <body>
20
+ <!-- This file lives in public/500.html -->
21
+ <div class="dialog">
22
+ <h1>We're sorry, but something went wrong.</h1>
23
+ </div>
24
+ </body>
25
+ </html>
File without changes
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application.
3
+
4
+ APP_PATH = File.expand_path('../../config/application', __FILE__)
5
+ require File.expand_path('../../config/boot', __FILE__)
6
+ require 'rails/commands'
@@ -0,0 +1,88 @@
1
+ require 'spec_helper'
2
+
3
+ describe Moribus::AliasAssociation do
4
+ before do
5
+ class SpecPost < MoribusSpecModel(:spec_author_id => :integer, :body => :string)
6
+ belongs_to :spec_author , :alias => :creator
7
+ has_many :spec_comments , :alias => :remarks
8
+ has_one :spec_post_info, :alias => :information
9
+
10
+ alias_association :author , :spec_author
11
+ alias_association :comments , :spec_comments
12
+ alias_association :post_info, :spec_post_info
13
+ end
14
+
15
+ class SpecAuthor < MoribusSpecModel(:name => :string)
16
+ has_many :spec_posts
17
+ end
18
+
19
+ class SpecPostInfo < MoribusSpecModel(:spec_post_id => :integer, :ip => :string)
20
+ belongs_to :spec_post, :alias => :note
21
+ end
22
+
23
+ class SpecComment < MoribusSpecModel(:spec_post_id => :integer, :body => :string)
24
+ belongs_to :spec_post
25
+ end
26
+ end
27
+
28
+ after do
29
+ MoribusSpecModel.cleanup!
30
+ end
31
+
32
+ before do
33
+ author = SpecAuthor.create(:name => 'John')
34
+ @post = author.spec_posts.create(:body => 'Post Body')
35
+ @post.spec_comments.create(:body => 'Fabulous!')
36
+ @post.create_spec_post_info(:ip => '127.0.0.1')
37
+ end
38
+
39
+ describe "reflection aliasing" do
40
+ it "alias association name in reflections" do
41
+ SpecPost.reflect_on_association(:author).should_not be_nil
42
+ end
43
+
44
+ it "should not raise error when using aliased name in scopes" do
45
+ expect{
46
+ SpecPost.includes(:comments).first
47
+ }.to_not raise_error
48
+ end
49
+ end
50
+
51
+ describe "association accessor alias methods" do
52
+ subject{ @post }
53
+
54
+ it{ should respond_to :author }
55
+ it{ should respond_to :author= }
56
+ it{ should respond_to :comments }
57
+ it{ should respond_to :comments= }
58
+ it{ should respond_to :post_info }
59
+ it{ should respond_to :post_info= }
60
+ end
61
+
62
+ describe "singular association alias method" do
63
+ subject{ @post }
64
+
65
+ it{ should respond_to :build_author }
66
+ it{ should respond_to :create_author }
67
+ it{ should respond_to :create_author! }
68
+
69
+ it{ should respond_to :build_post_info }
70
+ it{ should respond_to :create_post_info }
71
+ it{ should respond_to :create_post_info! }
72
+ end
73
+
74
+ describe "collection association alias method" do
75
+ subject{ @post }
76
+
77
+ it{ should respond_to :comment_ids }
78
+ it{ should respond_to :comment_ids= }
79
+ end
80
+
81
+ describe ":alias => alias_name shortcuts" do
82
+ subject{ @post }
83
+
84
+ it { should respond_to :creator }
85
+ it { should respond_to :remarks }
86
+ it { should respond_to :information }
87
+ end
88
+ end
@@ -0,0 +1,7 @@
1
+ require 'spec_helper'
2
+
3
+ # Some of the Moribus::Macros methods are tested in
4
+ # spec/lib/moribus_spec.rb.
5
+ describe Moribus::Macros do
6
+
7
+ end
@@ -0,0 +1,332 @@
1
+ require 'spec_helper'
2
+
3
+ describe Moribus do
4
+ before do
5
+ class SpecStatus < MoribusSpecModel(:name => :string, :description => :string)
6
+ acts_as_enumerated
7
+
8
+ self.enumeration_model_updates_permitted = true
9
+ create!(:name => 'inactive', :description => 'Inactive')
10
+ create!(:name => 'active', :description => 'Active')
11
+ end
12
+
13
+ class SpecType < MoribusSpecModel(:name => :string, :description => :string)
14
+ acts_as_enumerated
15
+
16
+ self.enumeration_model_updates_permitted = true
17
+ create!(:name => 'important', :description => 'Important')
18
+ create!(:name => 'unimportant', :description => 'Unimportant')
19
+ end
20
+
21
+ class SpecSuffix < MoribusSpecModel(:name => :string, :description => :string)
22
+ acts_as_enumerated
23
+
24
+ self.enumeration_model_updates_permitted = true
25
+ create!(:name => 'none', :description => '')
26
+ create!(:name => 'jr', :description => 'Junior')
27
+ end
28
+
29
+ class SpecPersonName < MoribusSpecModel(:first_name => :string, :last_name => :string, :spec_suffix_id => :integer)
30
+ acts_as_aggregated
31
+ has_enumerated :spec_suffix, :default => ''
32
+
33
+ validates_presence_of :first_name, :last_name
34
+
35
+ # custom writer that additionally strips first name
36
+ def first_name=(value)
37
+ self[:first_name] = value.strip
38
+ end
39
+ end
40
+
41
+ class SpecCustomerFeature < MoribusSpecModel(:feature_name => :string)
42
+ acts_as_aggregated :cache_by => :feature_name
43
+ end
44
+
45
+ class SpecCustomerInfo < MoribusSpecModel( :spec_customer_id => :integer!,
46
+ :spec_person_name_id => :integer,
47
+ :spec_status_id => :integer,
48
+ :spec_type_id => :integer,
49
+ :is_current => :boolean,
50
+ :lock_version => :integer,
51
+ :created_at => :datetime,
52
+ :updated_at => :datetime,
53
+ :previous_id => :integer )
54
+ attr :custom_field
55
+
56
+ belongs_to :spec_customer, :inverse_of => :spec_customer_info, :touch => true
57
+ has_aggregated :spec_person_name
58
+ has_enumerated :spec_status
59
+ has_enumerated :spec_type
60
+
61
+ acts_as_tracked :preceding_key => :previous_id
62
+ end
63
+
64
+ class SpecCustomer < MoribusSpecModel(:spec_status_id => :integer)
65
+ has_one_current :spec_customer_info, :inverse_of => :spec_customer
66
+ has_enumerated :spec_status, :default => 'inactive'
67
+
68
+ delegate_associated :spec_person_name, :custom_field, :spec_type, :to => :spec_customer_info
69
+ end
70
+
71
+ class SpecCustomerEmail < MoribusSpecModel(:spec_customer_id => :integer, :email => :string, :is_current => :boolean, :status => :string)
72
+ connection.add_index table_name, [:email, :is_current], :unique => true
73
+
74
+ belongs_to :spec_customer
75
+
76
+ acts_as_tracked
77
+ end
78
+ end
79
+
80
+ after do
81
+ MoribusSpecModel.cleanup!
82
+ end
83
+
84
+ describe "common behavior" do
85
+ before do
86
+ @info = SpecCustomerInfo.create(
87
+ :spec_customer_id => 1,
88
+ :spec_person_name_id => 1,
89
+ :is_current => true,
90
+ :created_at => 5.days.ago,
91
+ :updated_at => 5.days.ago
92
+ )
93
+ end
94
+
95
+ it "should revert changes if exception is raised" do
96
+ old_id = @info.id
97
+ old_updated_at = @info.updated_at
98
+ old_created_at = @info.created_at
99
+ suppress(Exception) do
100
+ expect {
101
+ @info.update_attributes :spec_customer_id => nil, :spec_person_name_id => 2
102
+ }.not_to change(SpecCustomerInfo, :count)
103
+ end
104
+ expect(@info.new_record?).to eq false
105
+ @info.id.should == old_id
106
+ @info.updated_at.should == old_updated_at
107
+ @info.created_at.should == old_created_at
108
+ end
109
+ end
110
+
111
+ describe 'Aggregated' do
112
+ context "definition" do
113
+ it "should raise an error on an unknown option" do
114
+ expect{
115
+ Class.new(ActiveRecord::Base).class_eval do
116
+ acts_as_aggregated :invalid_key => :error
117
+ end
118
+ }.to raise_error(ArgumentError)
119
+ end
120
+
121
+ it "should raise an error when including AggregatedCacheBehavior without AggregatedBehavior" do
122
+ expect{
123
+ Class.new(ActiveRecord::Base).class_eval do
124
+ include Moribus::AggregatedCacheBehavior
125
+ end
126
+ }.to raise_error(Moribus::AggregatedCacheBehavior::NotAggregatedError)
127
+ end
128
+ end
129
+
130
+ before do
131
+ @existing = SpecPersonName.create! :first_name => 'John', :last_name => 'Smith'
132
+ end
133
+
134
+ it "should not duplicate records" do
135
+ expect {
136
+ SpecPersonName.create :first_name => ' John ', :last_name => 'Smith'
137
+ }.not_to change(SpecPersonName, :count)
138
+ end
139
+
140
+ it "should lookup self and replace id with existing on create" do
141
+ name = SpecPersonName.new :first_name => 'John', :last_name => 'Smith'
142
+ name.save
143
+ name.id.should == @existing.id
144
+ end
145
+
146
+ it "should create a new record if lookup fails" do
147
+ expect {
148
+ SpecPersonName.create :first_name => 'Alice', :last_name => 'Smith'
149
+ }.to change(SpecPersonName, :count).by(1)
150
+ end
151
+
152
+ it "should lookup self and replace id with existing on update" do
153
+ name = SpecPersonName.create :first_name => 'Alice', :last_name => 'Smith'
154
+ name.update_attributes :first_name => 'John'
155
+ name.id.should == @existing.id
156
+ end
157
+
158
+ context "with caching" do
159
+ before do
160
+ @existing = SpecCustomerFeature.create(:feature_name => 'Pays')
161
+ SpecCustomerFeature.clear_cache
162
+ end
163
+
164
+ it "should lookup the existing value and add it to the cache" do
165
+ feature = SpecCustomerFeature.new :feature_name => @existing.feature_name
166
+ expect{ feature.save }.to change(SpecCustomerFeature.aggregated_records_cache, :length).by(1)
167
+ feature.id.should == @existing.id
168
+ end
169
+
170
+ it "should add the freshly-created record to the cache" do
171
+ expect{ SpecCustomerFeature.create(:feature_name => 'Fraud') }.to change(SpecCustomerFeature.aggregated_records_cache, :length).by(1)
172
+ end
173
+
174
+ it "should freeze the cached object" do
175
+ feature = SpecCustomerFeature.create(:feature_name => 'Cancelled')
176
+ SpecCustomerFeature.aggregated_records_cache[feature.feature_name].should be_frozen
177
+ end
178
+
179
+ it "should cache the clone of the record, not the record itself" do
180
+ feature = SpecCustomerFeature.create(:feature_name => 'Returned')
181
+ SpecCustomerFeature.aggregated_records_cache[feature.feature_name].object_id.should_not == feature.object_id
182
+ end
183
+ end
184
+ end
185
+
186
+ describe 'Tracked' do
187
+ before do
188
+ @customer = SpecCustomer.create
189
+ @info = @customer.create_spec_customer_info :spec_person_name_id => 1
190
+ end
191
+
192
+ it "should create a new current record if updated" do
193
+ expect {
194
+ @info.update_attributes(:spec_person_name_id => 2)
195
+ }.to change(SpecCustomerInfo, :count).by(1)
196
+ end
197
+
198
+ it "should replace itself with new id" do
199
+ old_id = @info.id
200
+ @info.update_attributes(:spec_person_name_id => 2)
201
+ @info.id.should_not == old_id
202
+ end
203
+
204
+ it "should set is_current record to false for superseded record" do
205
+ old_id = @info.id
206
+ @info.update_attributes(:spec_person_name_id => 2)
207
+ expect(SpecCustomerInfo.find(old_id).is_current).to eq false
208
+ end
209
+
210
+ it "should set previous_id to the id of the previous record" do
211
+ old_id = @info.id
212
+ @info.update_attributes(:spec_person_name_id => 2)
213
+ @info.previous_id.should == old_id
214
+ end
215
+
216
+ it "assigning a new current record should change is_current to false for previous one" do
217
+ new_info = SpecCustomerInfo.new :spec_person_name_id => 2, :is_current => true
218
+ @customer.spec_customer_info = new_info
219
+ new_info.spec_customer_id.should == @customer.id
220
+ expect(@info.is_current).to eq false
221
+ end
222
+
223
+ it "should not crash on superseding with 'is_current' conditional constraint" do
224
+ email = SpecCustomerEmail.create(:spec_customer => @customer, :email => 'foo@bar.com', :status => 'unverified', :is_current => true)
225
+ expect{ email.update_attributes(:status => 'verified') }.not_to raise_error
226
+ end
227
+
228
+ describe 'updated_at and created_at' do
229
+ let(:first_time) { Time.zone.parse('2012-07-16 00:00:00') }
230
+ let(:second_time) { Time.zone.parse('2012-07-17 08:10:15') }
231
+
232
+ before { Timecop.freeze(first_time) }
233
+ after { Timecop.return }
234
+
235
+ it "should be updated on change" do
236
+ info = @customer.create_spec_customer_info :spec_person_name_id => 1
237
+ info.updated_at.should == first_time
238
+ info.created_at.should == first_time
239
+
240
+ Timecop.freeze(second_time)
241
+ info.spec_person_name_id = 2
242
+ info.save!
243
+ info.updated_at.should == second_time
244
+ info.created_at.should == second_time
245
+ end
246
+ end
247
+
248
+ describe "Optimistic Locking" do
249
+ before do
250
+ @info1 = @customer.reload.spec_customer_info
251
+ @info2 = @customer.reload.spec_customer_info
252
+ end
253
+
254
+ it "should raise stale object error" do
255
+ @info1.update_attributes(:spec_person_name_id => 3)
256
+
257
+ expect{ @info2.update_attributes(:spec_person_name_id => 4) }.to raise_error(ActiveRecord::StaleObjectError)
258
+ end
259
+
260
+ it "should not fail if no locking_column present" do
261
+ email = SpecCustomerEmail.create(:spec_customer_id => 1, :email => 'foo@bar.com')
262
+ expect{ email.update_attributes(:email => 'foo2@bar.com') }.not_to raise_error
263
+ end
264
+ end
265
+
266
+ describe 'with Aggregated' do
267
+ before do
268
+ @info.spec_person_name = SpecPersonName.create(:first_name => 'John', :last_name => 'Smith')
269
+ @info.save
270
+ @info.reload
271
+ end
272
+
273
+ it "should supersede when nested record changes" do
274
+ old_id = @info.id
275
+ @customer.spec_customer_info.spec_person_name.first_name = 'Alice'
276
+ expect{ @customer.save }.to change(@info, :spec_person_name_id)
277
+ @info.id.should_not == old_id
278
+ @info.is_current.should == true
279
+ expect(SpecCustomerInfo.find(old_id).is_current).to eq false
280
+ end
281
+ end
282
+ end
283
+
284
+ describe 'Delegations' do
285
+ before do
286
+ @customer = SpecCustomer.create(
287
+ :spec_customer_info_attributes => {
288
+ :spec_person_name_attributes => {:first_name => ' John ', :last_name => 'Smith'} } )
289
+ @info = @customer.spec_customer_info
290
+ end
291
+
292
+ it "should have delegated column information" do
293
+ @customer.column_for_attribute(:first_name).should_not be_nil
294
+ end
295
+
296
+ it "should not delegate special methods" do
297
+ @customer.should_not respond_to(:reset_first_name)
298
+ @customer.should_not respond_to(:first_name_was)
299
+ @customer.should_not respond_to(:first_name_before_type_cast)
300
+ @customer.should_not respond_to(:first_name_will_change!)
301
+ @customer.should_not respond_to(:first_name_changed?)
302
+ @customer.should_not respond_to(:lock_version)
303
+ end
304
+
305
+ it "should delegate methods to aggregated parts" do
306
+ @info.should respond_to(:first_name)
307
+ @info.should respond_to(:first_name=)
308
+ @info.should respond_to(:spec_suffix)
309
+ @info.last_name.should == 'Smith'
310
+ end
311
+
312
+ it "should delegate methods to representation" do
313
+ @customer.should respond_to(:first_name)
314
+ @customer.should respond_to(:first_name=)
315
+ @customer.should respond_to(:spec_suffix)
316
+ @customer.last_name.should == 'Smith'
317
+ @customer.should respond_to(:custom_field)
318
+ @customer.should respond_to(:custom_field=)
319
+ end
320
+
321
+ it 'should properly delegate enumerated attributes' do
322
+ @customer.should respond_to(:spec_type)
323
+ @customer.should respond_to(:spec_type=)
324
+ @customer.spec_type = :important
325
+ @customer.spec_type.should === :important
326
+ end
327
+
328
+ it "should raise NoMethodError if unknown method received" do
329
+ expect{ @customer.impossibru }.to raise_error(NoMethodError)
330
+ end
331
+ end
332
+ end