ssickles-tire 0.4.2.7 → 0.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.
Files changed (59) hide show
  1. data/lib/tire.rb +18 -3
  2. data/lib/tire/alias.rb +11 -35
  3. data/lib/tire/index.rb +34 -76
  4. data/lib/tire/model/callbacks.rb +40 -0
  5. data/lib/tire/model/import.rb +26 -0
  6. data/lib/tire/model/indexing.rb +128 -0
  7. data/lib/tire/model/naming.rb +100 -0
  8. data/lib/tire/model/percolate.rb +99 -0
  9. data/lib/tire/model/persistence.rb +72 -0
  10. data/lib/tire/model/persistence/attributes.rb +143 -0
  11. data/lib/tire/model/persistence/finders.rb +66 -0
  12. data/lib/tire/model/persistence/storage.rb +71 -0
  13. data/lib/tire/model/search.rb +305 -0
  14. data/lib/tire/results/collection.rb +38 -13
  15. data/lib/tire/results/item.rb +19 -0
  16. data/lib/tire/rubyext/hash.rb +8 -0
  17. data/lib/tire/rubyext/ruby_1_8.rb +54 -0
  18. data/lib/tire/rubyext/symbol.rb +11 -0
  19. data/lib/tire/search.rb +7 -8
  20. data/lib/tire/search/scan.rb +8 -8
  21. data/lib/tire/search/sort.rb +1 -1
  22. data/lib/tire/utils.rb +17 -0
  23. data/lib/tire/version.rb +7 -38
  24. data/test/integration/active_model_indexing_test.rb +51 -0
  25. data/test/integration/active_model_searchable_test.rb +114 -0
  26. data/test/integration/active_record_searchable_test.rb +446 -0
  27. data/test/integration/mongoid_searchable_test.rb +309 -0
  28. data/test/integration/persistent_model_test.rb +117 -0
  29. data/test/integration/reindex_test.rb +2 -2
  30. data/test/integration/scan_test.rb +1 -1
  31. data/test/models/active_model_article.rb +31 -0
  32. data/test/models/active_model_article_with_callbacks.rb +49 -0
  33. data/test/models/active_model_article_with_custom_document_type.rb +7 -0
  34. data/test/models/active_model_article_with_custom_index_name.rb +7 -0
  35. data/test/models/active_record_models.rb +122 -0
  36. data/test/models/mongoid_models.rb +97 -0
  37. data/test/models/persistent_article.rb +11 -0
  38. data/test/models/persistent_article_in_namespace.rb +12 -0
  39. data/test/models/persistent_article_with_casting.rb +28 -0
  40. data/test/models/persistent_article_with_defaults.rb +11 -0
  41. data/test/models/persistent_articles_with_custom_index_name.rb +10 -0
  42. data/test/models/supermodel_article.rb +17 -0
  43. data/test/models/validated_model.rb +11 -0
  44. data/test/test_helper.rb +27 -3
  45. data/test/unit/active_model_lint_test.rb +17 -0
  46. data/test/unit/index_alias_test.rb +3 -17
  47. data/test/unit/index_test.rb +30 -18
  48. data/test/unit/model_callbacks_test.rb +116 -0
  49. data/test/unit/model_import_test.rb +71 -0
  50. data/test/unit/model_persistence_test.rb +516 -0
  51. data/test/unit/model_search_test.rb +899 -0
  52. data/test/unit/results_collection_test.rb +60 -0
  53. data/test/unit/results_item_test.rb +37 -0
  54. data/test/unit/rubyext_test.rb +3 -3
  55. data/test/unit/search_test.rb +1 -6
  56. data/test/unit/tire_test.rb +15 -0
  57. data/tire.gemspec +30 -13
  58. metadata +153 -41
  59. data/lib/tire/rubyext/to_json.rb +0 -21
@@ -0,0 +1,17 @@
1
+ require 'test_helper'
2
+
3
+ module Tire
4
+ module Model
5
+
6
+ class ActiveModelLintTest < Test::Unit::TestCase
7
+
8
+ include ActiveModel::Lint::Tests
9
+
10
+ def setup
11
+ @model = PersistentArticle.new :title => 'Test'
12
+ end
13
+
14
+ end
15
+
16
+ end
17
+ end
@@ -12,15 +12,9 @@ module Tire
12
12
  should "have a name and defaults" do
13
13
  @alias = Alias.new :name => 'dummy'
14
14
  assert_equal 'dummy', @alias.name
15
- assert_equal Configuration.url, @alias.url
16
15
  assert @alias.indices.empty?
17
16
  end
18
17
 
19
- should "have a configurable base URL" do
20
- @alias = Alias.new :url => 'http://example.com:9200'
21
- assert_equal 'http://example.com:9200', @alias.url
22
- end
23
-
24
18
  should "have indices" do
25
19
  @alias = Alias.new :indices => ['index_A', 'index_B']
26
20
  assert_equal ['index_A', 'index_B'], @alias.indices.to_a
@@ -100,7 +94,7 @@ module Tire
100
94
  a['add']['alias'] == 'alias_martha'
101
95
  end
102
96
  end.returns(mock_response('{}'), 200)
103
-
97
+
104
98
  a = Alias.find('alias_martha')
105
99
  a.indices.push 'index_A'
106
100
  a.save
@@ -115,7 +109,7 @@ module Tire
115
109
  a['remove']['alias'] == 'alias_martha'
116
110
  end
117
111
  end.returns(mock_response('{}'), 200)
118
-
112
+
119
113
  a = Alias.find('alias_martha')
120
114
  a.indices.delete 'index_A'
121
115
  a.save
@@ -126,7 +120,7 @@ module Tire
126
120
  # puts json
127
121
  MultiJson.decode(json)['actions'].all? { |a| a['add']['routing'] == 'martha' }
128
122
  end.returns(mock_response('{}'), 200)
129
-
123
+
130
124
  a = Alias.find('alias_martha')
131
125
  a.routing('martha')
132
126
  a.save
@@ -187,14 +181,6 @@ module Tire
187
181
  a = Alias.find('alias_martha')
188
182
  assert_instance_of Alias, a
189
183
  assert_equal ['index_B', 'index_C'], a.indices.to_a.sort
190
-
191
- Configuration.client.expects(:get).with do |url, json|
192
- url == "http://example.com:9200/_aliases"
193
- end.returns( mock_response( %q|{"index_A":{"aliases":{}},"index_B":{"aliases":{"alias_john":{"filter":{"term":{"user":"john"}}},"alias_martha":{"filter":{"term":{"user":"martha"}}}}},"index_C":{"aliases":{"alias_martha":{"filter":{"term":{"user":"martha"}}}}}}|), 200 )
194
-
195
- a = Alias.find('http://example.com:9200', 'alias_martha')
196
- assert_instance_of Alias, a
197
- assert_equal ['index_B', 'index_C'], a.indices.to_a.sort
198
184
  end
199
185
 
200
186
  should "find an alias and configure it with a block" do
@@ -18,11 +18,6 @@ module Tire
18
18
  assert_equal "#{Configuration.url}/#{@index.name}", @index.url
19
19
  end
20
20
 
21
- should "have a configurable URL endpoint" do
22
- @index.url = 'http://example.com'
23
- assert_equal "http://example.com/#{@index.name}", @index.url
24
- end
25
-
26
21
  should "return HTTP response" do
27
22
  assert_respond_to @index, :response
28
23
 
@@ -216,21 +211,21 @@ module Tire
216
211
 
217
212
  should "set type from Hash :type property" do
218
213
  Configuration.client.expects(:post).with do |url,document|
219
- url == "#{@index.url}/article"
214
+ url == "#{@index.url}/article/"
220
215
  end.returns(mock_response('{"ok":true,"_id":"test"}'))
221
216
  @index.store :type => 'article', :title => 'Test'
222
217
  end
223
218
 
224
219
  should "set type from Hash :_type property" do
225
220
  Configuration.client.expects(:post).with do |url,document|
226
- url == "#{@index.url}/article"
221
+ url == "#{@index.url}/article/"
227
222
  end.returns(mock_response('{"ok":true,"_id":"test"}'))
228
223
  @index.store :_type => 'article', :title => 'Test'
229
224
  end
230
225
 
231
226
  should "set type from Object _type method" do
232
227
  Configuration.client.expects(:post).with do |url,document|
233
- url == "#{@index.url}/article"
228
+ url == "#{@index.url}/article/"
234
229
  end.returns(mock_response('{"ok":true,"_id":"test"}'))
235
230
 
236
231
  article = Class.new do
@@ -242,7 +237,7 @@ module Tire
242
237
 
243
238
  should "set type from Object type method" do
244
239
  Configuration.client.expects(:post).with do |url,document|
245
- url == "#{@index.url}/article"
240
+ url == "#{@index.url}/article/"
246
241
  end.returns(mock_response('{"ok":true,"_id":"test"}'))
247
242
 
248
243
  article = Class.new do
@@ -254,7 +249,7 @@ module Tire
254
249
 
255
250
  should "properly encode namespaced document types" do
256
251
  Configuration.client.expects(:post).with do |url,document|
257
- url == "#{@index.url}/my_namespace%2Fmy_model"
252
+ url == "#{@index.url}/my_namespace%2Fmy_model/"
258
253
  end.returns(mock_response('{"ok":true,"_id":"123"}'))
259
254
 
260
255
  module MyNamespace
@@ -268,7 +263,7 @@ module Tire
268
263
  end
269
264
 
270
265
  should "set default type" do
271
- Configuration.client.expects(:post).with("#{@index.url}/document", '{"title":"Test"}').returns(mock_response('{"ok":true,"_id":"test"}'))
266
+ Configuration.client.expects(:post).with("#{@index.url}/document/", '{"title":"Test"}').returns(mock_response('{"ok":true,"_id":"test"}'))
272
267
  @index.store :title => 'Test'
273
268
  end
274
269
 
@@ -315,7 +310,7 @@ module Tire
315
310
  Configuration.reset :wrapper
316
311
 
317
312
  Configuration.client.stubs(:post).with do |url, payload|
318
- url == "#{@index.url}/article" &&
313
+ url == "#{@index.url}/article/" &&
319
314
  payload =~ /"title":"Test"/
320
315
  end.
321
316
  returns(mock_response('{"ok":true,"_id":"id-1"}'))
@@ -452,6 +447,23 @@ module Tire
452
447
  @index.bulk_store [ {:id => '1', :title => 'One'}, {:id => '2', :title => 'Two'} ]
453
448
  end
454
449
 
450
+ should "serialize ActiveModel instances" do
451
+ Configuration.client.expects(:post).with do |url, json|
452
+ url == "#{ActiveModelArticle.index.url}/_bulk" &&
453
+ json =~ /"_index":"active_model_articles"/ &&
454
+ json =~ /"_type":"active_model_article"/ &&
455
+ json =~ /"_id":"1"/ &&
456
+ json =~ /"_id":"2"/ &&
457
+ json =~ /"title":"One"/ &&
458
+ json =~ /"title":"Two"/
459
+ end.returns(mock_response('{}', 200))
460
+
461
+ one = ActiveModelArticle.new 'title' => 'One'; one.id = '1'
462
+ two = ActiveModelArticle.new 'title' => 'Two'; two.id = '2'
463
+
464
+ ActiveModelArticle.index.bulk_store [ one, two ]
465
+ end
466
+
455
467
  context "namespaced models" do
456
468
  should "not URL-escape the document_type" do
457
469
  Configuration.client.expects(:post).with do |url, json|
@@ -502,13 +514,13 @@ module Tire
502
514
 
503
515
  should "display error message when collection item does not have ID" do
504
516
  Configuration.client.expects(:post).with do |url, json|
505
- url == "#{@index.url}/_bulk"
517
+ url == "#{ActiveModelArticle.index.url}/_bulk"
506
518
  end.returns(mock_response('success', 200))
507
519
 
508
520
  STDERR.expects(:puts).once
509
521
 
510
522
  documents = [ { :title => 'Bogus' }, { :title => 'Real', :id => 1 } ]
511
- @index.bulk_store documents
523
+ ActiveModelArticle.index.bulk_store documents
512
524
  end
513
525
 
514
526
  end
@@ -733,7 +745,7 @@ module Tire
733
745
  should "percolate document against all registered queries" do
734
746
  Configuration.client.expects(:post).
735
747
  with do |url, payload|
736
- url == "#{@index.url}/article?percolate=%2A" &&
748
+ url == "#{@index.url}/article/?percolate=*" &&
737
749
  payload =~ /"title":"Test"/
738
750
  end.
739
751
  returns(mock_response('{"ok":true,"_id":"test","matches":["alerts"]}'))
@@ -743,7 +755,7 @@ module Tire
743
755
  should "percolate document against specific queries" do
744
756
  Configuration.client.expects(:post).
745
757
  with do |url, payload|
746
- url == "#{@index.url}/article?percolate=tag%3Aalerts" &&
758
+ url == "#{@index.url}/article/?percolate=tag:alerts" &&
747
759
  payload =~ /"title":"Test"/
748
760
  end.
749
761
  returns(mock_response('{"ok":true,"_id":"test","matches":["alerts"]}'))
@@ -811,10 +823,10 @@ module Tire
811
823
  def index_something
812
824
  @tags = ['block', 'scope', 'revenge']
813
825
 
814
- Index.any_instance.expects(:store).with(:title => 'Title From Outer Space', :tags => ['block', 'scope', 'revenge'])
826
+ Index.any_instance.expects(:store).with(title: 'Title From Outer Space', tags: ['block', 'scope', 'revenge'])
815
827
 
816
828
  Tire::Index.new 'outer-space' do |index|
817
- index.store(:title => @my_title, :tags => @tags)
829
+ index.store title: @my_title, tags: @tags
818
830
  end
819
831
  end
820
832
 
@@ -0,0 +1,116 @@
1
+ require 'test_helper'
2
+
3
+ class ModelOne
4
+ extend ActiveModel::Naming
5
+ include Tire::Model::Search
6
+ include Tire::Model::Callbacks
7
+
8
+ def save; false; end
9
+ def destroy; false; end
10
+ end
11
+
12
+ class ModelTwo
13
+ extend ActiveModel::Naming
14
+ extend ActiveModel::Callbacks
15
+ define_model_callbacks :save, :destroy
16
+
17
+ include Tire::Model::Search
18
+ include Tire::Model::Callbacks
19
+
20
+ def save; _run_save_callbacks {}; end
21
+ def destroy; _run_destroy_callbacks { @destroyed = true }; end
22
+
23
+ def destroyed?; !!@destroyed; end
24
+ end
25
+
26
+ class ModelThree
27
+ extend ActiveModel::Naming
28
+ extend ActiveModel::Callbacks
29
+ define_model_callbacks :save, :destroy
30
+
31
+ include Tire::Model::Search
32
+ include Tire::Model::Callbacks
33
+
34
+ def save; _run_save_callbacks {}; end
35
+ def destroy; _run_destroy_callbacks {}; end
36
+ end
37
+
38
+ class ModelWithoutTireAutoCallbacks
39
+ extend ActiveModel::Naming
40
+ extend ActiveModel::Callbacks
41
+ define_model_callbacks :save, :destroy
42
+
43
+ include Tire::Model::Search
44
+ # DO NOT include Callbacks
45
+
46
+ def save; _run_save_callbacks {}; end
47
+ def destroy; _run_destroy_callbacks {}; end
48
+ end
49
+
50
+ module Tire
51
+ module Model
52
+
53
+ class ModelCallbacksTest < Test::Unit::TestCase
54
+
55
+ context "Model without ActiveModel callbacks" do
56
+
57
+ should "not execute any callbacks" do
58
+ m = ModelOne.new
59
+ m.tire.expects(:update_index).never
60
+
61
+ m.save
62
+ m.destroy
63
+ end
64
+
65
+ end
66
+
67
+ context "Model with ActiveModel callbacks and implemented destroyed? method" do
68
+
69
+ should "execute the callbacks" do
70
+ m = ModelTwo.new
71
+ m.tire.expects(:update_index).twice
72
+
73
+ m.save
74
+ m.destroy
75
+ end
76
+
77
+ end
78
+
79
+ context "Model with ActiveModel callbacks without destroyed? method implemented" do
80
+
81
+ should "have the destroyed? method added" do
82
+ assert_respond_to ModelThree.new, :destroyed?
83
+ end
84
+
85
+ should "execute the callbacks" do
86
+ m = ModelThree.new
87
+ m.tire.expects(:update_index).twice
88
+
89
+ m.save
90
+ m.destroy
91
+ end
92
+
93
+ end
94
+
95
+ context "Model without Tire::Callbacks included" do
96
+
97
+ should "respond to Tire update_index callbacks" do
98
+ assert_respond_to ModelWithoutTireAutoCallbacks, :after_update_elasticsearch_index
99
+ assert_respond_to ModelWithoutTireAutoCallbacks, :before_update_elasticsearch_index
100
+ end
101
+
102
+ should "not execute the update_index hooks" do
103
+ m = ModelWithoutTireAutoCallbacks.new
104
+ m.tire.expects(:update_index).never
105
+
106
+ m.save
107
+ m.destroy
108
+ end
109
+ end
110
+
111
+ # ---------------------------------------------------------------------------
112
+
113
+ end
114
+
115
+ end
116
+ end
@@ -0,0 +1,71 @@
1
+ require 'test_helper'
2
+
3
+ class ImportModel
4
+ extend ActiveModel::Naming
5
+ include Tire::Model::Search
6
+ include Tire::Model::Callbacks
7
+
8
+ DATA = (1..4).to_a
9
+
10
+ def self.paginate(options={})
11
+ options = {:page => 1, :per_page => 1000}.update options
12
+ DATA.slice( (options[:page]-1)*options[:per_page]...options[:page]*options[:per_page] )
13
+ end
14
+
15
+ def self.all(options={})
16
+ DATA
17
+ end
18
+
19
+ def self.count
20
+ DATA.size
21
+ end
22
+ end
23
+
24
+ module Tire
25
+ module Model
26
+
27
+ class ImportTest < Test::Unit::TestCase
28
+
29
+ context "Model::Import" do
30
+
31
+ should "have the import method" do
32
+ assert_respond_to ImportModel, :import
33
+ end
34
+
35
+ should "paginate the results by default when importing" do
36
+ Tire::Index.any_instance.expects(:bulk_store).returns(true).times(2)
37
+
38
+ ImportModel.import :per_page => 2
39
+ end
40
+
41
+ should "call the passed block on every batch, and NOT manipulate the documents array" do
42
+ Tire::Index.any_instance.expects(:bulk_store).with { |c,o| c == [1, 2] }
43
+ Tire::Index.any_instance.expects(:bulk_store).with { |c,o| c == [3, 4] }
44
+
45
+ runs = 0
46
+ ImportModel.import :per_page => 2 do |documents|
47
+ runs += 1
48
+ # Don't forget to return the documents at the end of the block
49
+ documents
50
+ end
51
+
52
+ assert_equal 2, runs
53
+ end
54
+
55
+ should "manipulate the documents in passed block" do
56
+ Tire::Index.any_instance.expects(:bulk_store).with { |c,o| c == [2, 3] }
57
+ Tire::Index.any_instance.expects(:bulk_store).with { |c,o| c == [4, 5] }
58
+
59
+ ImportModel.import :per_page => 2 do |documents|
60
+ # Add 1 to every "document" and return them
61
+ documents.map { |d| d + 1 }
62
+ end
63
+
64
+ end
65
+
66
+ end
67
+
68
+ end
69
+
70
+ end
71
+ end
@@ -0,0 +1,516 @@
1
+ require 'test_helper'
2
+
3
+ module Tire
4
+ module Model
5
+
6
+ class PersistenceTest < Test::Unit::TestCase
7
+
8
+ context "Model" do
9
+
10
+ should "have default index name" do
11
+ assert_equal 'persistent_articles', PersistentArticle.index_name
12
+ assert_equal 'persistent_articles', PersistentArticle.new(:title => 'Test').index_name
13
+ end
14
+
15
+ should "allow to set custom index name" do
16
+ assert_equal 'custom-index-name', PersistentArticleWithCustomIndexName.index_name
17
+
18
+ PersistentArticleWithCustomIndexName.index_name "another-index-name"
19
+ assert_equal 'another-index-name', PersistentArticleWithCustomIndexName.index_name
20
+ assert_equal 'another-index-name', PersistentArticleWithCustomIndexName.index.name
21
+ end
22
+
23
+ context "with index prefix" do
24
+ setup do
25
+ Model::Search.index_prefix 'prefix'
26
+ end
27
+
28
+ teardown do
29
+ Model::Search.index_prefix nil
30
+ end
31
+
32
+ should "have configured prefix in index_name" do
33
+ assert_equal 'prefix_persistent_articles', PersistentArticle.index_name
34
+ assert_equal 'prefix_persistent_articles', PersistentArticle.new(:title => 'Test').index_name
35
+ end
36
+
37
+ end
38
+
39
+ should "have document_type" do
40
+ assert_equal 'persistent_article', PersistentArticle.document_type
41
+ assert_equal 'persistent_article', PersistentArticle.new(:title => 'Test').document_type
42
+ end
43
+
44
+ should "allow to define property" do
45
+ assert_nothing_raised do
46
+ a = PersistentArticle.new
47
+ class << a
48
+ property :status
49
+ end
50
+ end
51
+ end
52
+
53
+ end
54
+
55
+ context "Finders" do
56
+
57
+ setup do
58
+ @first = { '_id' => 1, '_version' => 1, '_index' => 'persistent_articles', '_type' => 'persistent_article', '_source' => { :title => 'First' } }
59
+ @second = { '_id' => 2, '_index' => 'persistent_articles', '_type' => 'persistent_article', '_source' => { :title => 'Second' } }
60
+ @third = { '_id' => 3, '_index' => 'persistent_articles', '_type' => 'persistent_article', '_source' => { :title => 'Third' } }
61
+ @find_all = { 'hits' => { 'hits' => [
62
+ @first,
63
+ @second,
64
+ @third
65
+ ] } }
66
+ @find_first = { 'hits' => { 'hits' => [ @first ] } }
67
+ @find_last_two = { 'hits' => { 'hits' => [ @second, @third ] } }
68
+ @find_twenty_ids = { 'hits' => { 'hits' => 20.times.map { @first } } }
69
+ end
70
+
71
+ should "find document by numeric ID" do
72
+ Configuration.client.expects(:get).returns(mock_response(@first.to_json))
73
+ document = PersistentArticle.find 1
74
+
75
+ assert_instance_of PersistentArticle, document
76
+ assert_equal 1, document.id
77
+ assert_equal 1, document.attributes['id']
78
+ assert_equal 'First', document.attributes['title']
79
+ assert_equal 'First', document.title
80
+ end
81
+
82
+ should "have _type, _index, _id, _version attributes" do
83
+ Configuration.client.expects(:get).returns(mock_response(@first.to_json))
84
+ document = PersistentArticle.find 1
85
+
86
+ assert_instance_of PersistentArticle, document
87
+ assert_equal 1, document.id
88
+ assert_equal 1, document.attributes['id']
89
+ assert_equal 'persistent_articles', document._index
90
+ assert_equal 'persistent_article', document._type
91
+ assert_equal 1, document._version
92
+ end
93
+
94
+ should "find document by string ID" do
95
+ Configuration.client.expects(:get).returns(mock_response(@first.to_json))
96
+ document = PersistentArticle.find '1'
97
+
98
+ assert_instance_of PersistentArticle, document
99
+ assert_equal 1, document.id
100
+ assert_equal 1, document.attributes['id']
101
+ assert_equal 'First', document.attributes['title']
102
+ assert_equal 'First', document.title
103
+ end
104
+
105
+ should "find document by list of IDs" do
106
+ Configuration.client.expects(:get).returns(mock_response(@find_last_two.to_json))
107
+ documents = PersistentArticle.find 2, 3
108
+
109
+ assert_equal 2, documents.count
110
+ end
111
+
112
+ should "find document by array of IDs" do
113
+ Configuration.client.expects(:get).returns(mock_response(@find_last_two.to_json))
114
+ documents = PersistentArticle.find [2, 3]
115
+
116
+ assert_equal 2, documents.count
117
+ end
118
+
119
+ should "find all documents listed in IDs array" do
120
+ ids = (1..20).to_a
121
+ Configuration.client.expects(:get).returns(mock_response(@find_twenty_ids.to_json))
122
+ Tire::Search::Search.any_instance.expects(:size).with(ids.size)
123
+
124
+ documents = PersistentArticle.find ids
125
+ assert_equal ids.size, documents.count
126
+ end
127
+
128
+ should "find all documents" do
129
+ Configuration.client.stubs(:get).returns(mock_response(@find_all.to_json))
130
+ documents = PersistentArticle.all
131
+
132
+ assert_equal 3, documents.count
133
+ assert_equal 'First', documents.first.attributes['title']
134
+ assert_equal PersistentArticle.find(:all).map { |e| e.id }, PersistentArticle.all.map { |e| e.id }
135
+ end
136
+
137
+ should "find first document" do
138
+ Configuration.client.expects(:get).returns(mock_response(@find_first.to_json))
139
+ document = PersistentArticle.first
140
+
141
+ assert_equal 'First', document.attributes['title']
142
+ end
143
+
144
+ should "raise error when passing incorrect argument" do
145
+ assert_raise(ArgumentError) do
146
+ PersistentArticle.find :name => 'Test'
147
+ end
148
+ end
149
+
150
+ should_eventually "raise error when document is not found" do
151
+ assert_raise(DocumentNotFound) do
152
+ PersistentArticle.find 'xyz001'
153
+ end
154
+ end
155
+
156
+ end
157
+
158
+ context "Persistent model" do
159
+
160
+ setup { @article = PersistentArticle.new :title => 'Test', :tags => [:one, :two] }
161
+
162
+ context "attribute methods" do
163
+
164
+ should "allow to set attributes on initialization" do
165
+ assert_not_nil @article.attributes
166
+ assert_equal 'Test', @article.attributes['title']
167
+ end
168
+
169
+ should "allow to leave attributes blank on initialization" do
170
+ assert_nothing_raised { PersistentArticle.new }
171
+ end
172
+
173
+ should "have getter methods for attributes" do
174
+ assert_not_nil @article.title
175
+ assert_equal 'Test', @article.title
176
+ assert_equal [:one, :two], @article.tags
177
+ end
178
+
179
+ should "have getter methods for attribute passed as a String" do
180
+ article = PersistentArticle.new 'title' => 'Tony Montana'
181
+ assert_not_nil article.title
182
+ assert_equal 'Tony Montana', article.title
183
+ end
184
+
185
+ should "raise error when getting unknown attribute" do
186
+ assert_raise(NoMethodError) do
187
+ @article.krapulitz
188
+ end
189
+ end
190
+
191
+ should "not raise error when getting unset attribute" do
192
+ article = PersistentArticle.new :title => 'Test'
193
+
194
+ assert_nothing_raised { article.published_on }
195
+ assert_nil article.published_on
196
+ end
197
+
198
+ should "return default value for attribute" do
199
+ article = PersistentArticleWithDefaults.new :title => 'Test'
200
+ assert_equal [], article.tags
201
+ assert_equal false, article.hidden
202
+ end
203
+
204
+ should "not affect default value" do
205
+ article = PersistentArticleWithDefaults.new :title => 'Test'
206
+ article.tags << "ruby"
207
+
208
+ article.options[:switches] << "switch_1"
209
+
210
+ assert_equal [], PersistentArticleWithDefaults.new.tags
211
+ assert_equal [], PersistentArticleWithDefaults.new.options[:switches]
212
+ end
213
+
214
+ should "have query method for attribute" do
215
+ assert_equal true, @article.title?
216
+ end
217
+
218
+ should "raise error when querying for unknown attribute" do
219
+ assert_raise(NoMethodError) do
220
+ @article.krapulitz?
221
+ end
222
+ end
223
+
224
+ should "not raise error when querying for unset attribute" do
225
+ article = PersistentArticle.new :title => 'Test'
226
+
227
+ assert_nothing_raised { article.published_on? }
228
+ assert ! article.published_on?
229
+ end
230
+
231
+ should "return true for respond_to? calls for set attributes" do
232
+ article = PersistentArticle.new :title => 'Test'
233
+ assert article.respond_to?(:title)
234
+ end
235
+
236
+ should "return false for respond_to? calls for unknown attributes" do
237
+ article = PersistentArticle.new :title => 'Test'
238
+ assert ! article.respond_to?(:krapulitz)
239
+ end
240
+
241
+ should "return true for respond_to? calls for defined but unset attributes" do
242
+ article = PersistentArticle.new :title => 'Test'
243
+
244
+ assert article.respond_to?(:published_on)
245
+ end
246
+
247
+ should "have attribute names" do
248
+ article = PersistentArticle.new :title => 'Test', :tags => ['one', 'two']
249
+ assert_equal ['published_on', 'tags', 'title'], article.attribute_names
250
+ end
251
+
252
+ should "have setter method for attribute" do
253
+ @article.title = 'Updated'
254
+ assert_equal 'Updated', @article.title
255
+ assert_equal 'Updated', @article.attributes['title']
256
+ end
257
+
258
+ should_eventually "allow to set deeply nested attributes on initialization" do
259
+ article = PersistentArticle.new :title => 'Test', :author => { :first_name => 'John', :last_name => 'Smith' }
260
+
261
+ assert_equal 'John', article.author.first_name
262
+ assert_equal 'Smith', article.author.last_name
263
+ assert_equal({ :first_name => 'John', :last_name => 'Smith' }, article.attributes['author'])
264
+ end
265
+
266
+ should_eventually "allow to set deeply nested attributes on update" do
267
+ article = PersistentArticle.new :title => 'Test', :author => { :first_name => 'John', :last_name => 'Smith' }
268
+
269
+ article.author.first_name = 'Robert'
270
+ article.author.last_name = 'Carpenter'
271
+
272
+ assert_equal 'Robert', article.author.first_name
273
+ assert_equal 'Carpenter', article.author.last_name
274
+ assert_equal({ :first_name => 'Robert', :last_name => 'Carpenter' }, article.attributes['author'])
275
+ end
276
+
277
+ end
278
+
279
+ context "with casting" do
280
+
281
+ should "cast the value as custom class" do
282
+ article = PersistentArticleWithCastedItem.new :title => 'Test',
283
+ :author => { :first_name => 'John', :last_name => 'Smith' }
284
+ assert_instance_of Author, article.author
285
+ assert_equal 'John', article.author.first_name
286
+ end
287
+
288
+ should "cast the value as collection of custom classes" do
289
+ article = PersistentArticleWithCastedCollection.new :title => 'Test',
290
+ :comments => [{:nick => '4chan', :body => 'WHY U NO?'}]
291
+ assert_instance_of Array, article.comments
292
+ assert_instance_of Comment, article.comments.first
293
+ assert_equal '4chan', article.comments.first.nick
294
+ end
295
+
296
+ should "automatically format strings in UTC format as Time" do
297
+ article = PersistentArticle.new :published_on => '2011-11-01T23:00:00Z'
298
+ assert_instance_of Time, article.published_on
299
+ assert_equal 2011, article.published_on.year
300
+ end
301
+
302
+ should "cast anonymous Hashes as Hashr instances" do
303
+ article = PersistentArticleWithCastedItem.new :stats => { :views => 100, :meta => { :tags => 'A' } }
304
+ assert_equal 100, article.stats.views
305
+ assert_equal 'A', article.stats.meta.tags
306
+ end
307
+
308
+ end
309
+
310
+ context "when creating" do
311
+
312
+ should "save the document with generated ID in the database" do
313
+ Configuration.client.expects(:post).
314
+ with do |url, payload|
315
+ doc = MultiJson.decode(payload)
316
+ url == "#{Configuration.url}/persistent_articles/persistent_article/" &&
317
+ doc['title'] == 'Test' &&
318
+ doc['tags'] == ['one', 'two']
319
+ doc['published_on'] == nil
320
+ end.
321
+ returns(mock_response('{"ok":true,"_id":"abc123"}'))
322
+ article = PersistentArticle.create :title => 'Test', :tags => [:one, :two]
323
+
324
+ assert article.persisted?, "#{article.inspect} should be `persisted?`"
325
+ assert_equal 'abc123', article.id
326
+ end
327
+
328
+ should "save the document with custom ID in the database" do
329
+ Configuration.client.expects(:post).
330
+ with do |url, payload|
331
+ doc = MultiJson.decode(payload)
332
+ url == "#{Configuration.url}/persistent_articles/persistent_article/r2d2" &&
333
+ doc['title'] == 'Test' &&
334
+ doc['published_on'] == nil
335
+ end.
336
+ returns(mock_response('{"ok":true,"_id":"r2d2"}'))
337
+ article = PersistentArticle.create :id => 'r2d2', :title => 'Test'
338
+
339
+ assert_equal 'r2d2', article.id
340
+ end
341
+
342
+ should "perform model validations" do
343
+ Configuration.client.expects(:post).never
344
+
345
+ assert ! ValidatedModel.create(:name => nil)
346
+ end
347
+
348
+ end
349
+
350
+ context "when creating" do
351
+
352
+ should "set the id property" do
353
+ Configuration.client.expects(:post).
354
+ with do |url, payload|
355
+ doc = MultiJson.decode(payload)
356
+ url == "#{Configuration.url}/persistent_articles/persistent_article/" &&
357
+ doc['title'] == 'Test'
358
+ end.
359
+ returns(mock_response('{"ok":true,"_id":"1"}'))
360
+
361
+ article = PersistentArticle.create :title => 'Test'
362
+ assert_equal '1', article.id
363
+ end
364
+
365
+ should "not set the id property if already set" do
366
+ Configuration.client.expects(:post).
367
+ with do |url, payload|
368
+ doc = MultiJson.decode(payload)
369
+ url == "#{Configuration.url}/persistent_articles/persistent_article/123" &&
370
+ doc['title'] == 'Test' &&
371
+ doc['published_on'] == nil
372
+ end.
373
+ returns(mock_response('{"ok":true, "_id":"XXX"}'))
374
+
375
+ article = PersistentArticle.create :id => '123', :title => 'Test'
376
+ assert_equal '123', article.id
377
+ end
378
+
379
+ end
380
+
381
+ context "when saving" do
382
+
383
+ should "save the document with updated attribute" do
384
+ article = PersistentArticle.new :id => '1', :title => 'Test'
385
+
386
+ Configuration.client.expects(:post).
387
+ with do |url, payload|
388
+ doc = MultiJson.decode(payload)
389
+ url == "#{Configuration.url}/persistent_articles/persistent_article/1" &&
390
+ doc['title'] == 'Test' &&
391
+ doc['published_on'] == nil
392
+ end.
393
+ returns(mock_response('{"ok":true,"_id":"1"}'))
394
+ assert article.save
395
+
396
+ article.title = 'Updated'
397
+
398
+ Configuration.client.expects(:post).
399
+ with do |url, payload|
400
+ doc = MultiJson.decode(payload)
401
+ url == "#{Configuration.url}/persistent_articles/persistent_article/1" &&
402
+ doc['title'] == 'Updated'
403
+ end.
404
+ returns(mock_response('{"ok":true,"_id":"1"}'))
405
+ assert article.save
406
+ end
407
+
408
+ should "perform validations" do
409
+ article = ValidatedModel.new :name => nil
410
+ assert ! article.save
411
+ end
412
+
413
+ should "set the id property itself" do
414
+ article = PersistentArticle.new
415
+ article.title = 'Test'
416
+
417
+ Configuration.client.expects(:post).with("#{Configuration.url}/persistent_articles/persistent_article/",
418
+ article.to_indexed_json).
419
+ returns(mock_response('{"ok":true,"_id":"1"}'))
420
+ assert article.save
421
+ assert_equal '1', article.id
422
+ end
423
+
424
+ should "not set the id property if already set" do
425
+ article = PersistentArticle.new
426
+ article.id = '456'
427
+ article.title = 'Test'
428
+
429
+ Configuration.client.expects(:post).
430
+ with do |url, payload|
431
+ doc = MultiJson.decode(payload)
432
+ url == "#{Configuration.url}/persistent_articles/persistent_article/456" &&
433
+ doc['title'] == 'Test'
434
+ end.
435
+ returns(mock_response('{"ok":true,"_id":"XXX"}'))
436
+ assert article.save
437
+ assert_equal '456', article.id
438
+ end
439
+
440
+ end
441
+
442
+ context "when destroying" do
443
+
444
+ should "delete the document from the database" do
445
+ Configuration.client.expects(:post).
446
+ with do |url, payload|
447
+ doc = MultiJson.decode(payload)
448
+ url == "#{Configuration.url}/persistent_articles/persistent_article/123" &&
449
+ doc['title'] == 'Test'
450
+ end.returns(mock_response('{"ok":true,"_id":"123"}'))
451
+
452
+ Configuration.client.expects(:delete).
453
+ with("#{Configuration.url}/persistent_articles/persistent_article/123").
454
+ returns(mock_response('{"ok":true,"acknowledged":true}', 200))
455
+
456
+ article = PersistentArticle.new :id => '123', :title => 'Test'
457
+ article.save
458
+ article.destroy
459
+ end
460
+
461
+ end
462
+
463
+ context "when updating attributes" do
464
+
465
+ should "update single attribute" do
466
+ @article.expects(:save).returns(true)
467
+
468
+ @article.update_attribute :title, 'Updated'
469
+ assert_equal 'Updated', @article.title
470
+ end
471
+
472
+ should "update all attributes" do
473
+ @article.expects(:save).returns(true)
474
+
475
+ @article.update_attributes :title => 'Updated', :tags => ['three']
476
+ assert_equal 'Updated', @article.title
477
+ assert_equal ['three'], @article.tags
478
+ end
479
+
480
+ end
481
+
482
+ end
483
+
484
+ context "Persistent model with mapping definition" do
485
+
486
+ should "create the index with mapping" do
487
+ expected = {
488
+ :settings => {},
489
+ :mappings => { :persistent_article_with_mapping => {
490
+ :properties => { :title => { :type => 'string', :analyzer => 'snowball', :boost => 10 } }
491
+ }}
492
+ }
493
+
494
+ Tire::Index.any_instance.stubs(:exists?).returns(false)
495
+ Tire::Index.any_instance.expects(:create).with(expected)
496
+
497
+ class ::PersistentArticleWithMapping
498
+
499
+ include Tire::Model::Persistence
500
+ include Tire::Model::Search
501
+ include Tire::Model::Callbacks
502
+
503
+ mapping do
504
+ property :title, :type => 'string', :analyzer => 'snowball', :boost => 10
505
+ end
506
+
507
+ end
508
+
509
+ assert_equal 'snowball', PersistentArticleWithMapping.mapping[:title][:analyzer]
510
+ end
511
+
512
+ end
513
+
514
+ end
515
+ end
516
+ end