guacamole 0.0.1 → 0.1.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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/{config/rubocop.yml → .hound.yml} +1 -12
  4. data/.ruby-version +1 -1
  5. data/.travis.yml +2 -0
  6. data/.yardopts +1 -0
  7. data/CONTRIBUTING.md +3 -3
  8. data/Gemfile.devtools +24 -12
  9. data/Guardfile +1 -1
  10. data/README.md +347 -50
  11. data/Rakefile +10 -0
  12. data/config/reek.yml +18 -5
  13. data/guacamole.gemspec +5 -2
  14. data/lib/guacamole.rb +1 -0
  15. data/lib/guacamole/collection.rb +79 -7
  16. data/lib/guacamole/configuration.rb +56 -2
  17. data/lib/guacamole/document_model_mapper.rb +87 -7
  18. data/lib/guacamole/identity_map.rb +124 -0
  19. data/lib/guacamole/proxies/proxy.rb +42 -0
  20. data/lib/guacamole/proxies/referenced_by.rb +15 -0
  21. data/lib/guacamole/proxies/references.rb +15 -0
  22. data/lib/guacamole/query.rb +11 -0
  23. data/lib/guacamole/railtie.rb +6 -1
  24. data/lib/guacamole/railtie/database.rake +57 -3
  25. data/lib/guacamole/tasks/database.rake +23 -0
  26. data/lib/guacamole/version.rb +1 -1
  27. data/lib/rails/generators/guacamole/collection/collection_generator.rb +19 -0
  28. data/lib/rails/generators/guacamole/collection/templates/collection.rb.tt +5 -0
  29. data/lib/rails/generators/guacamole/config/config_generator.rb +25 -0
  30. data/lib/rails/generators/guacamole/config/templates/guacamole.yml +15 -0
  31. data/lib/rails/generators/guacamole/model/model_generator.rb +25 -0
  32. data/lib/rails/generators/guacamole/model/templates/model.rb.tt +11 -0
  33. data/lib/rails/generators/guacamole_generator.rb +28 -0
  34. data/lib/rails/generators/rails/collection/collection_generator.rb +13 -0
  35. data/lib/rails/generators/rspec/collection/collection_generator.rb +13 -0
  36. data/lib/rails/generators/rspec/collection/templates/collection_spec.rb.tt +7 -0
  37. data/spec/acceptance/association_spec.rb +40 -0
  38. data/spec/acceptance/basic_spec.rb +19 -2
  39. data/spec/acceptance/spec_helper.rb +5 -2
  40. data/spec/fabricators/author.rb +11 -0
  41. data/spec/fabricators/author_fabricator.rb +7 -0
  42. data/spec/fabricators/book.rb +11 -0
  43. data/spec/fabricators/book_fabricator.rb +5 -0
  44. data/spec/unit/collection_spec.rb +265 -18
  45. data/spec/unit/configuration_spec.rb +11 -1
  46. data/spec/unit/document_model_mapper_spec.rb +127 -5
  47. data/spec/unit/identiy_map_spec.rb +140 -0
  48. data/spec/unit/query_spec.rb +37 -16
  49. data/tasks/adjustments.rake +0 -1
  50. metadata +78 -8
@@ -0,0 +1,28 @@
1
+ # -*- encoding : utf-8 -*-
2
+
3
+ require 'rails/generators/named_base'
4
+ require 'rails/generators/active_model'
5
+
6
+ module Guacamole
7
+ module Generators
8
+ class Base < ::Rails::Generators::NamedBase
9
+ def self.source_root
10
+ @_guacamole_source_root ||=
11
+ File.expand_path("../#{base_name}/#{generator_name}/templates", __FILE__)
12
+ end
13
+ end
14
+ end
15
+ end
16
+
17
+ module Rails
18
+ module Generators
19
+ class GeneratedAttribute
20
+ def type_class
21
+ return 'Time' if type == :datetime
22
+ return 'String' if type == :text
23
+ return 'Boolean' if type == :boolean
24
+ type.to_s.camelcase
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,13 @@
1
+ # -*- encoding : utf-8 -*-
2
+
3
+ require 'rails/generators/guacamole/collection/collection_generator.rb'
4
+
5
+ module Rails
6
+ module Generators
7
+ class CollectionGenerator < Guacamole::Generators::CollectionGenerator
8
+ def self.source_root
9
+ File.expand_path("../../guacamole/#{generator_name}/templates", File.dirname(__FILE__))
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ # -*- encoding : utf-8 -*-
2
+
3
+ require 'rails/generators/guacamole_generator'
4
+
5
+ module Rspec
6
+ module Generators
7
+ class CollectionGenerator < ::Guacamole::Generators::Base
8
+ def create_collection_spec
9
+ template 'collection_spec.rb.tt', File.join('spec/collections', class_path, "#{file_name}_collection_spec.rb")
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,7 @@
1
+ require 'spec_helper'
2
+
3
+ <% module_namespacing do -%>
4
+ describe <%= class_name %>Collection do
5
+ pending "add some examples to (or delete) #{__FILE__}"
6
+ end
7
+ <% end -%>
@@ -0,0 +1,40 @@
1
+ # -*- encoding : utf-8 -*-
2
+ require 'guacamole'
3
+ require 'acceptance/spec_helper'
4
+
5
+ require 'fabricators/book'
6
+ require 'fabricators/author'
7
+
8
+ class AuthorsCollection
9
+ include Guacamole::Collection
10
+
11
+ map do
12
+ referenced_by :books
13
+ end
14
+ end
15
+
16
+ class BooksCollection
17
+ include Guacamole::Collection
18
+
19
+ map do
20
+ references :author
21
+ end
22
+ end
23
+
24
+ describe 'Associations' do
25
+ let(:author) { Fabricate(:author_with_three_books) }
26
+
27
+ it 'should load referenced models from the database' do
28
+ the_author = AuthorsCollection.by_key author.key
29
+ books_from_author = BooksCollection.by_example(author_id: author.key).to_a
30
+
31
+ expect(books_from_author).to eq the_author.books.to_a
32
+ end
33
+
34
+ it 'should load the referenced model from the database' do
35
+ the_author = AuthorsCollection.by_key author.key
36
+ a_book_by_the_author = BooksCollection.by_example(author_id: author.key).to_a.first
37
+
38
+ expect(a_book_by_the_author.author).to eq the_author
39
+ end
40
+ end
@@ -75,7 +75,7 @@ describe 'CollectionBasics' do
75
75
  expect(found_model).to eq some_article
76
76
  end
77
77
 
78
- it 'should save models to the database' do
78
+ it 'should create models in the database' do
79
79
  new_article = Fabricate.build(:article)
80
80
  subject.save new_article
81
81
 
@@ -84,7 +84,7 @@ describe 'CollectionBasics' do
84
84
 
85
85
  it 'should update models in the database' do
86
86
  some_article.title = 'Has been updated'
87
- subject.replace some_article
87
+ subject.save some_article
88
88
 
89
89
  updated_article = subject.by_key(some_article.key)
90
90
 
@@ -107,6 +107,23 @@ describe 'CollectionBasics' do
107
107
  expect(found_article.comments.first).to be_a Comment
108
108
  expect(found_article.comments).to eq article_with_comments.comments
109
109
  end
110
+
111
+ context 'prevent aliasing effects' do
112
+ it 'should hold only one object of the same document when getting one document' do
113
+ this_article = subject.by_key some_article.key
114
+ that_article = subject.by_key some_article.key
115
+
116
+ expect(this_article.object_id).to eq that_article.object_id
117
+ end
118
+
119
+ it 'should only hold one object of the same document when using `by_example`' do
120
+ Fabricate.times(3, :article)
121
+
122
+ subject.all.each do |article|
123
+ expect(article.object_id).to eq subject.by_key(article.key).object_id
124
+ end
125
+ end
126
+ end
110
127
  end
111
128
 
112
129
  end
@@ -4,6 +4,7 @@ $LOAD_PATH.unshift(File.dirname(__FILE__))
4
4
  $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
5
5
 
6
6
  require 'fabrication'
7
+ require 'faker'
7
8
  require 'logging'
8
9
  require 'ashikawa-core'
9
10
 
@@ -13,8 +14,11 @@ rescue LoadError
13
14
  puts "Debugger is not available. Maybe you're Travis."
14
15
  end
15
16
 
16
- class Fabrication::Generator::Guacamole < Fabrication::Generator::Base
17
+ # This is required to remove the deprecation warning introduced in this commit:
18
+ # https://github.com/svenfuchs/i18n/commit/3b6e56e06fd70f6e4507996b017238505e66608c.
19
+ I18n.config.enforce_available_locales = false
17
20
 
21
+ class Fabrication::Generator::Guacamole < Fabrication::Generator::Base
18
22
  def self.supports?(klass)
19
23
  defined?(Guacamole) && klass.ancestors.include?(Guacamole::Model)
20
24
  end
@@ -27,7 +31,6 @@ class Fabrication::Generator::Guacamole < Fabrication::Generator::Base
27
31
  def validate_instance
28
32
  _instance.valid?
29
33
  end
30
-
31
34
  end
32
35
 
33
36
  Fabrication::Schematic::Definition::GENERATORS.unshift Fabrication::Generator::Guacamole
@@ -0,0 +1,11 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ class Author
4
+ extend ActiveSupport::Autoload
5
+ include Guacamole::Model
6
+
7
+ autoload :Book, 'fabricators/book'
8
+
9
+ attribute :name, String
10
+ attribute :books, Array[Book], coerce: false
11
+ end
@@ -0,0 +1,7 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Fabricator(:author_with_three_books, from: :author) do
4
+ name 'Star Swirl the Bearded'
5
+
6
+ books(count: 3)
7
+ end
@@ -0,0 +1,11 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ class Book
4
+ include Guacamole::Model
5
+ include Guacamole::Model
6
+
7
+ autoload :Author, 'fabricators/author'
8
+
9
+ attribute :title, String
10
+ attribute :author, Author
11
+ end
@@ -0,0 +1,5 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Fabricator(:book) do
4
+ title { Faker::Lorem.words.join(' ') }
5
+ end
@@ -115,9 +115,174 @@ describe Guacamole::Collection do
115
115
 
116
116
  describe 'save' do
117
117
 
118
+ before do
119
+ allow(mapper).to receive(:model_to_document).with(model).and_return(document)
120
+ allow(mapper).to receive(:referenced_by_models).and_return([])
121
+ allow(mapper).to receive(:referenced_models).and_return([])
122
+ end
123
+
124
+ let(:key) { double('Key') }
125
+ let(:rev) { double('Rev') }
126
+ let(:document) { double('Document', key: key, revision: rev).as_null_object }
127
+ let(:model) { double('Model').as_null_object }
128
+ let(:response) { double('Hash') }
129
+
130
+ context 'a valid model' do
131
+
132
+ before do
133
+ allow(model).to receive(:valid?).and_return(true)
134
+ end
135
+
136
+ context 'which is not persisted' do
137
+
138
+ before do
139
+ allow(connection).to receive(:create_document).with(document).and_return(document)
140
+ allow(model).to receive(:persisted?).and_return(false)
141
+ end
142
+
143
+ it 'should return the model after calling save' do
144
+ expect(subject.save(model)).to eq model
145
+ end
146
+
147
+ it 'should set timestamps before creating the document' do
148
+ now = double('Time.now')
149
+
150
+ allow(Time).to receive(:now).once.and_return(now)
151
+
152
+ expect(model).to receive(:created_at=).with(now).ordered
153
+ expect(model).to receive(:updated_at=).with(now).ordered
154
+
155
+ allow(connection).to receive(:create_document).with(document).and_return(document).ordered
156
+
157
+ subject.save model
158
+ end
159
+
160
+ it 'should add key to model' do
161
+ expect(model).to receive(:key=).with(key)
162
+
163
+ subject.save model
164
+ end
165
+
166
+ it 'should add rev to model' do
167
+ expect(model).to receive(:rev=).with(rev)
168
+
169
+ subject.save model
170
+ end
171
+ end
172
+
173
+ context 'which is persisted' do
174
+
175
+ before do
176
+ allow(model).to receive(:persisted?).and_return(true)
177
+ allow(connection).to receive(:replace).and_return(response)
178
+ allow(response).to receive(:[]).with('_rev').and_return(rev)
179
+ end
180
+
181
+ let(:model) { double('Model', key: key).as_null_object }
182
+
183
+ it 'should set the updated_at timestamp before replacing the document' do
184
+ now = double('Time.now')
185
+
186
+ allow(Time).to receive(:now).once.and_return(now)
187
+ expect(model).to receive(:updated_at=).with(now)
188
+
189
+ subject.save model
190
+ end
191
+
192
+ it 'should replace the document by key via the connection' do
193
+ expect(connection).to receive(:replace).with(key, document)
194
+
195
+ subject.save model
196
+ end
197
+
198
+ it 'should update the revision after replacing the document' do
199
+ allow(connection).to receive(:replace).and_return(response).ordered
200
+ expect(model).to receive(:rev=).with(rev).ordered
201
+
202
+ subject.save model
203
+ end
204
+
205
+ it 'should return the model' do
206
+ expect(subject.save(model)).to eq model
207
+ end
208
+
209
+ it 'should not update created_at' do
210
+ expect(model).not_to receive(:created_at=)
211
+
212
+ subject.save model
213
+ end
214
+
215
+ end
216
+ end
217
+
218
+ context 'an invalid model' do
219
+
220
+ before do
221
+ expect(model).to receive(:valid?).and_return(false)
222
+ end
223
+
224
+ context 'which is not persisted' do
225
+
226
+ before do
227
+ allow(model).to receive(:persisted?).and_return(false)
228
+ end
229
+
230
+ it 'should not be used to create the document' do
231
+ expect(connection).not_to receive(:create_document)
232
+
233
+ subject.save model
234
+ end
235
+
236
+ it 'should not be changed' do
237
+ expect(model).not_to receive(:created_at=)
238
+ expect(model).not_to receive(:updated_at=)
239
+ expect(model).not_to receive(:key=)
240
+ expect(model).not_to receive(:rev=)
241
+
242
+ subject.save model
243
+ end
244
+
245
+ it 'should return false' do
246
+ expect(subject.save(model)).to be false
247
+ end
248
+ end
249
+
250
+ context 'which is persisted' do
251
+
252
+ before do
253
+ allow(model).to receive(:persisted?).and_return(true)
254
+ allow(response).to receive(:[]).with('_rev').and_return(rev)
255
+ end
256
+
257
+ let(:model) { double('Model', key: key).as_null_object }
258
+
259
+ it 'should not be used to replace the document' do
260
+ expect(connection).not_to receive(:replace)
261
+
262
+ subject.save model
263
+ end
264
+
265
+ it 'should not be changed' do
266
+ expect(model).not_to receive(:rev=)
267
+ expect(model).not_to receive(:updated_at=)
268
+
269
+ subject.save model
270
+ end
271
+
272
+ it 'should return false' do
273
+ expect(subject.save(model)).to be false
274
+ end
275
+ end
276
+ end
277
+ end
278
+
279
+ describe 'create' do
280
+
118
281
  before do
119
282
  allow(connection).to receive(:create_document).with(document).and_return(document)
120
283
  allow(mapper).to receive(:model_to_document).with(model).and_return(document)
284
+ allow(mapper).to receive(:referenced_by_models).and_return([])
285
+ allow(mapper).to receive(:referenced_models).and_return([])
121
286
  end
122
287
 
123
288
  let(:key) { double('Key') }
@@ -134,11 +299,11 @@ describe Guacamole::Collection do
134
299
  expect(connection).to receive(:create_document).with(document).and_return(document)
135
300
  expect(mapper).to receive(:model_to_document).with(model).and_return(document)
136
301
 
137
- subject.save model
302
+ subject.create model
138
303
  end
139
304
 
140
- it 'should return the model after calling save' do
141
- expect(subject.save(model)).to eq model
305
+ it 'should return the model after calling create' do
306
+ expect(subject.create(model)).to eq model
142
307
  end
143
308
 
144
309
  it 'should set timestamps before creating the document' do
@@ -151,19 +316,101 @@ describe Guacamole::Collection do
151
316
 
152
317
  allow(connection).to receive(:create_document).with(document).and_return(document).ordered
153
318
 
154
- subject.save model
319
+ subject.create model
155
320
  end
156
321
 
157
322
  it 'should add key to model' do
158
323
  expect(model).to receive(:key=).with(key)
159
324
 
160
- subject.save model
325
+ subject.create model
161
326
  end
162
327
 
163
328
  it 'should add rev to model' do
164
329
  expect(model).to receive(:rev=).with(rev)
165
330
 
166
- subject.save model
331
+ subject.create model
332
+ end
333
+
334
+ context 'with referenced model' do
335
+ let(:referenced_model) { double('ReferencedModel') }
336
+ let(:referenced_model_name) { :some_referenced_model }
337
+ let(:referenced_models) { [referenced_model_name] }
338
+ let(:collection_for_referenced_model) { double('Collection') }
339
+
340
+ before do
341
+ allow(referenced_model).to receive(:persisted?).and_return(false)
342
+ allow(mapper).to receive(:collection_for)
343
+ .with(referenced_model_name)
344
+ .and_return(collection_for_referenced_model)
345
+ allow(mapper).to receive(:referenced_models).and_return(referenced_models)
346
+ allow(model).to receive(:send).with(referenced_model_name).and_return(referenced_model)
347
+ allow(collection_for_referenced_model).to receive(:save).with(referenced_model)
348
+ end
349
+
350
+ it 'should save each referenced model' do
351
+ expect(collection_for_referenced_model).to receive(:save).with(referenced_model)
352
+
353
+ subject.create model
354
+ end
355
+
356
+ it 'should save the reference only if it is not already persisted' do
357
+ allow(referenced_model).to receive(:persisted?).and_return(true)
358
+ expect(collection_for_referenced_model).not_to receive(:save).with(referenced_model)
359
+
360
+ subject.create model
361
+ end
362
+
363
+ it 'should save the reference only if it is present' do
364
+ allow(model).to receive(:send).with(:some_referenced_model).and_return(nil)
365
+ expect(collection_for_referenced_model).not_to receive(:save).with(referenced_model)
366
+
367
+ subject.create model
368
+ end
369
+ end
370
+
371
+ context 'with referenced_by model' do
372
+ let(:referenced_by_model) { double('ReferencedByModel') }
373
+ let(:referenced_by_model_name) { :some_referenced_by_model }
374
+ let(:referenced_by_models) { [referenced_by_model_name] }
375
+ let(:collection_for_referenced_by_model) { double('Collection') }
376
+
377
+ before do
378
+ allow(referenced_by_model).to receive(:persisted?).and_return(false)
379
+ allow(referenced_by_model).to receive("#{model.class.name.demodulize.underscore}=").with(model)
380
+ allow(mapper).to receive(:collection_for)
381
+ .with(referenced_by_model_name)
382
+ .and_return(collection_for_referenced_by_model)
383
+ allow(mapper).to receive(:referenced_by_models).and_return(referenced_by_models)
384
+ allow(model).to receive(:send).with(referenced_by_model_name).and_return([referenced_by_model])
385
+ allow(collection_for_referenced_by_model).to receive(:save).with(referenced_by_model)
386
+ end
387
+
388
+ it 'should save the references' do
389
+ expect(collection_for_referenced_by_model).to receive(:save).with(referenced_by_model)
390
+
391
+ subject.create model
392
+ end
393
+
394
+ it 'should save the references only if it is not already persisted' do
395
+ allow(referenced_by_model).to receive(:persisted?).and_return(true)
396
+ expect(collection_for_referenced_by_model).not_to receive(:save).with(referenced_by_model)
397
+
398
+ subject.create model
399
+ end
400
+
401
+ it 'should save the references only if it is present' do
402
+ allow(model).to receive(:send).with(referenced_by_model_name).and_return([])
403
+ expect(collection_for_referenced_by_model).not_to receive(:save).with(referenced_by_model)
404
+
405
+ subject.create model
406
+ end
407
+
408
+ it 'should ensure the references have the parent model assigned prior saving' do
409
+ expect(referenced_by_model).to receive("#{model.class.name.demodulize.underscore}=").with(model).ordered
410
+ expect(collection_for_referenced_by_model).to receive(:save).with(referenced_by_model).ordered
411
+
412
+ subject.create model
413
+ end
167
414
  end
168
415
  end
169
416
 
@@ -174,22 +421,22 @@ describe Guacamole::Collection do
174
421
  end
175
422
 
176
423
  it 'should not be used to create the document' do
177
- expect(connection).to receive(:create_document).never
424
+ expect(connection).not_to receive(:create_document)
178
425
 
179
- subject.save model
426
+ subject.create model
180
427
  end
181
428
 
182
429
  it 'should not be changed' do
183
- expect(model).to receive(:created_at=).never
184
- expect(model).to receive(:updated_at=).never
185
- expect(model).to receive(:key=).never
186
- expect(model).to receive(:rev=).never
430
+ expect(model).not_to receive(:created_at=)
431
+ expect(model).not_to receive(:updated_at=)
432
+ expect(model).not_to receive(:key=)
433
+ expect(model).not_to receive(:rev=)
187
434
 
188
- subject.save model
435
+ subject.create model
189
436
  end
190
437
 
191
438
  it 'should return false' do
192
- expect(subject.save(model)).to be false
439
+ expect(subject.create(model)).to be false
193
440
  end
194
441
  end
195
442
  end
@@ -274,7 +521,7 @@ describe Guacamole::Collection do
274
521
  end
275
522
 
276
523
  it 'should not update created_at' do
277
- expect(model).to receive(:created_at=).never
524
+ expect(model).not_to receive(:created_at=)
278
525
 
279
526
  subject.replace model
280
527
  end
@@ -286,14 +533,14 @@ describe Guacamole::Collection do
286
533
  end
287
534
 
288
535
  it 'should not be used to replace the document' do
289
- expect(connection).to receive(:replace).never
536
+ expect(connection).not_to receive(:replace)
290
537
 
291
538
  subject.replace model
292
539
  end
293
540
 
294
541
  it 'should not be changed' do
295
- expect(model).to receive(:rev=).never
296
- expect(model).to receive(:updated_at=).never
542
+ expect(model).not_to receive(:rev=)
543
+ expect(model).not_to receive(:updated_at=)
297
544
 
298
545
  subject.replace model
299
546
  end