tire 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 (83) hide show
  1. data/.gitignore +9 -0
  2. data/Gemfile +4 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.markdown +435 -0
  5. data/Rakefile +75 -0
  6. data/examples/dsl.rb +73 -0
  7. data/examples/rails-application-template.rb +144 -0
  8. data/examples/tire-dsl.rb +617 -0
  9. data/lib/tire.rb +35 -0
  10. data/lib/tire/client.rb +40 -0
  11. data/lib/tire/configuration.rb +29 -0
  12. data/lib/tire/dsl.rb +33 -0
  13. data/lib/tire/index.rb +209 -0
  14. data/lib/tire/logger.rb +60 -0
  15. data/lib/tire/model/callbacks.rb +23 -0
  16. data/lib/tire/model/import.rb +18 -0
  17. data/lib/tire/model/indexing.rb +50 -0
  18. data/lib/tire/model/naming.rb +30 -0
  19. data/lib/tire/model/persistence.rb +34 -0
  20. data/lib/tire/model/persistence/attributes.rb +60 -0
  21. data/lib/tire/model/persistence/finders.rb +61 -0
  22. data/lib/tire/model/persistence/storage.rb +75 -0
  23. data/lib/tire/model/search.rb +97 -0
  24. data/lib/tire/results/collection.rb +56 -0
  25. data/lib/tire/results/item.rb +39 -0
  26. data/lib/tire/results/pagination.rb +30 -0
  27. data/lib/tire/rubyext/hash.rb +3 -0
  28. data/lib/tire/rubyext/symbol.rb +11 -0
  29. data/lib/tire/search.rb +117 -0
  30. data/lib/tire/search/facet.rb +41 -0
  31. data/lib/tire/search/filter.rb +28 -0
  32. data/lib/tire/search/highlight.rb +37 -0
  33. data/lib/tire/search/query.rb +42 -0
  34. data/lib/tire/search/sort.rb +29 -0
  35. data/lib/tire/tasks.rb +88 -0
  36. data/lib/tire/version.rb +3 -0
  37. data/test/fixtures/articles/1.json +1 -0
  38. data/test/fixtures/articles/2.json +1 -0
  39. data/test/fixtures/articles/3.json +1 -0
  40. data/test/fixtures/articles/4.json +1 -0
  41. data/test/fixtures/articles/5.json +1 -0
  42. data/test/integration/active_model_searchable_test.rb +80 -0
  43. data/test/integration/active_record_searchable_test.rb +193 -0
  44. data/test/integration/facets_test.rb +65 -0
  45. data/test/integration/filters_test.rb +46 -0
  46. data/test/integration/highlight_test.rb +52 -0
  47. data/test/integration/index_mapping_test.rb +44 -0
  48. data/test/integration/index_store_test.rb +68 -0
  49. data/test/integration/persistent_model_test.rb +35 -0
  50. data/test/integration/query_string_test.rb +43 -0
  51. data/test/integration/results_test.rb +28 -0
  52. data/test/integration/sort_test.rb +36 -0
  53. data/test/models/active_model_article.rb +31 -0
  54. data/test/models/active_model_article_with_callbacks.rb +49 -0
  55. data/test/models/active_model_article_with_custom_index_name.rb +5 -0
  56. data/test/models/active_record_article.rb +12 -0
  57. data/test/models/article.rb +15 -0
  58. data/test/models/persistent_article.rb +11 -0
  59. data/test/models/persistent_articles_with_custom_index_name.rb +10 -0
  60. data/test/models/supermodel_article.rb +22 -0
  61. data/test/models/validated_model.rb +11 -0
  62. data/test/test_helper.rb +52 -0
  63. data/test/unit/active_model_lint_test.rb +17 -0
  64. data/test/unit/client_test.rb +43 -0
  65. data/test/unit/configuration_test.rb +71 -0
  66. data/test/unit/index_test.rb +390 -0
  67. data/test/unit/logger_test.rb +114 -0
  68. data/test/unit/model_callbacks_test.rb +90 -0
  69. data/test/unit/model_import_test.rb +71 -0
  70. data/test/unit/model_persistence_test.rb +400 -0
  71. data/test/unit/model_search_test.rb +289 -0
  72. data/test/unit/results_collection_test.rb +131 -0
  73. data/test/unit/results_item_test.rb +59 -0
  74. data/test/unit/rubyext_hash_test.rb +19 -0
  75. data/test/unit/search_facet_test.rb +69 -0
  76. data/test/unit/search_filter_test.rb +36 -0
  77. data/test/unit/search_highlight_test.rb +46 -0
  78. data/test/unit/search_query_test.rb +55 -0
  79. data/test/unit/search_sort_test.rb +50 -0
  80. data/test/unit/search_test.rb +204 -0
  81. data/test/unit/tire_test.rb +55 -0
  82. data/tire.gemspec +54 -0
  83. metadata +372 -0
@@ -0,0 +1,114 @@
1
+ require 'test_helper'
2
+ require 'time'
3
+
4
+ module Tire
5
+
6
+ class LoggerTest < Test::Unit::TestCase
7
+ include Tire
8
+
9
+ context "Logger" do
10
+
11
+ context "initialized with an IO object" do
12
+
13
+ should "take STDOUT" do
14
+ assert_nothing_raised do
15
+ logger = Logger.new STDOUT
16
+ end
17
+ end
18
+
19
+ should "write to STDERR" do
20
+ STDERR.expects(:write).with('BOOM!')
21
+ logger = Logger.new STDERR
22
+ logger.write('BOOM!')
23
+ end
24
+
25
+ end
26
+
27
+ context "initialized with file" do
28
+ teardown { File.delete('myfile.log') }
29
+
30
+ should "create the file" do
31
+ assert_nothing_raised do
32
+ logger = Logger.new 'myfile.log'
33
+ assert File.exists?('myfile.log')
34
+ end
35
+ end
36
+
37
+ should "write to file" do
38
+ File.any_instance.expects(:write).with('BOOM!')
39
+ logger = Logger.new 'myfile.log'
40
+ logger.write('BOOM!')
41
+ end
42
+
43
+ end
44
+
45
+ end
46
+
47
+ context "levels" do
48
+
49
+ should "have the default level" do
50
+ logger = Logger.new STDERR
51
+ assert_equal 'info', logger.level
52
+ end
53
+
54
+ should "set the level" do
55
+ logger = Logger.new STDERR, :level => 'debug'
56
+ assert_equal 'debug', logger.level
57
+ end
58
+
59
+ end
60
+
61
+ context "tracing requests" do
62
+ setup do
63
+ Time.stubs(:now).returns(Time.parse('2011-03-19 11:00'))
64
+ @logger = Logger.new STDERR
65
+ end
66
+
67
+ should "log request in correct format" do
68
+ log = (<<-"log;").gsub(/^ +/, '')
69
+ # 2011-03-19 11:00:00:L [_search] (["articles", "users"])
70
+ #
71
+ curl -X GET http://...
72
+
73
+ log;
74
+ @logger.expects(:write).with(log)
75
+ @logger.log_request('_search', ["articles", "users"], 'curl -X GET http://...')
76
+ end
77
+
78
+ should "log response in correct format" do
79
+ json = (<<-"json;").gsub(/^\s*/, '')
80
+ {
81
+ "took" : 4,
82
+ "hits" : {
83
+ "total" : 20,
84
+ "max_score" : 1.0,
85
+ "hits" : [ {
86
+ "_index" : "articles",
87
+ "_type" : "article",
88
+ "_id" : "Hmg0B0VSRKm2VAlsasdnqg",
89
+ "_score" : 1.0, "_source" : { "title" : "Article 1", "published_on" : "2011-01-01" }
90
+ }, {
91
+ "_index" : "articles",
92
+ "_type" : "article",
93
+ "_id" : "booSWC8eRly2I06GTUilNA",
94
+ "_score" : 1.0, "_source" : { "title" : "Article 2", "published_on" : "2011-01-12" }
95
+ }
96
+ ]
97
+ }
98
+ }
99
+ json;
100
+ log = (<<-"log;").gsub(/^\s*/, '')
101
+ # 2011-03-19 11:00:00:L [200 OK] (4 msec)
102
+ #
103
+ log;
104
+ # log += json.split.map { |line| "# #{line}" }.join("\n")
105
+ json.each_line { |line| log += "# #{line}" }
106
+ log += "\n\n"
107
+ @logger.expects(:write).with(log)
108
+ @logger.log_response('200 OK', 4, json)
109
+ end
110
+
111
+ end
112
+
113
+ end
114
+ end
@@ -0,0 +1,90 @@
1
+ require 'test_helper'
2
+
3
+ class ModelOne
4
+ include Tire::Model::Search
5
+ include Tire::Model::Callbacks
6
+
7
+ def save; false; end
8
+ def destroy; false; end
9
+ end
10
+
11
+ class ModelTwo
12
+ extend ActiveModel::Callbacks
13
+ define_model_callbacks :save, :destroy
14
+
15
+ include Tire::Model::Search
16
+ include Tire::Model::Callbacks
17
+
18
+ def save
19
+ _run_save_callbacks {}
20
+ end
21
+
22
+ def destroy
23
+ _run_destroy_callbacks { @destroyed = true }
24
+ end
25
+
26
+ def destroyed?; !!@destroyed; end
27
+ end
28
+
29
+ class ModelThree
30
+ extend ActiveModel::Callbacks
31
+ define_model_callbacks :save, :destroy
32
+
33
+ include Tire::Model::Search
34
+ include Tire::Model::Callbacks
35
+
36
+ def save
37
+ _run_save_callbacks {}
38
+ end
39
+
40
+ def destroy
41
+ _run_destroy_callbacks {}
42
+ end
43
+ end
44
+
45
+ module Tire
46
+ module Model
47
+
48
+ class ModelCallbacksTest < Test::Unit::TestCase
49
+
50
+ context "Model without ActiveModel callbacks" do
51
+
52
+ should "not execute any callbacks" do
53
+ ModelOne.any_instance.expects(:update_elastic_search_index).never
54
+
55
+ ModelOne.new.save
56
+ ModelOne.new.destroy
57
+ end
58
+
59
+ end
60
+
61
+ context "Model with ActiveModel callbacks and implemented destroyed? method" do
62
+
63
+ should "execute the callbacks" do
64
+ ModelTwo.any_instance.expects(:update_elastic_search_index).twice
65
+
66
+ ModelTwo.new.save
67
+ ModelTwo.new.destroy
68
+ end
69
+
70
+ end
71
+
72
+ context "Model with ActiveModel callbacks without destroyed? method implemented" do
73
+
74
+ should "have the destroyed? method added" do
75
+ assert_respond_to ModelThree.new, :destroyed?
76
+ end
77
+
78
+ should "execute the callbacks" do
79
+ ModelThree.any_instance.expects(:update_elastic_search_index).twice
80
+
81
+ ModelThree.new.save
82
+ ModelThree.new.destroy
83
+ end
84
+
85
+ end
86
+
87
+ end
88
+
89
+ end
90
+ 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([1, 2])
43
+ Tire::Index.any_instance.expects(:bulk_store).with([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([2, 3])
57
+ Tire::Index.any_instance.expects(:bulk_store).with([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,400 @@
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
+ should "have document_type" do
24
+ assert_equal 'persistent_article', PersistentArticle.document_type
25
+ assert_equal 'persistent_article', PersistentArticle.new(:title => 'Test').document_type
26
+ end
27
+
28
+ should "allow to define property" do
29
+ assert_nothing_raised do
30
+ a = PersistentArticle.new
31
+ class << a
32
+ property :status
33
+ end
34
+ end
35
+ end
36
+
37
+ end
38
+
39
+ context "Finders" do
40
+
41
+ setup do
42
+ @first = { '_id' => 1, '_source' => { :title => 'First' } }
43
+ @second = { '_id' => 2, '_source' => { :title => 'Second' } }
44
+ @third = { '_id' => 3, '_source' => { :title => 'Third' } }
45
+ @find_all = { 'hits' => { 'hits' => [
46
+ @first,
47
+ @second,
48
+ @third
49
+ ] } }
50
+ @find_first = { 'hits' => { 'hits' => [ @first ] } }
51
+ @find_last_two = { 'hits' => { 'hits' => [ @second, @third ] } }
52
+ end
53
+
54
+ should "find document by numeric ID" do
55
+ Configuration.client.expects(:get).returns(mock_response(@first.to_json))
56
+ document = PersistentArticle.find 1
57
+
58
+ assert_instance_of PersistentArticle, document
59
+ assert_equal 1, document.id
60
+ assert_equal 1, document.attributes['id']
61
+ assert_equal 'First', document.attributes['title']
62
+ assert_equal 'First', document.title
63
+ end
64
+
65
+ should "find document by string ID" do
66
+ Configuration.client.expects(:get).returns(mock_response(@first.to_json))
67
+ document = PersistentArticle.find '1'
68
+
69
+ assert_instance_of PersistentArticle, document
70
+ assert_equal 1, document.id
71
+ assert_equal 1, document.attributes['id']
72
+ assert_equal 'First', document.attributes['title']
73
+ assert_equal 'First', document.title
74
+ end
75
+
76
+ should "find document by list of IDs" do
77
+ Configuration.client.expects(:post).returns(mock_response(@find_last_two.to_json))
78
+ documents = PersistentArticle.find 2, 3
79
+
80
+ assert_equal 2, documents.count
81
+ end
82
+
83
+ should "find document by array of IDs" do
84
+ Configuration.client.expects(:post).returns(mock_response(@find_last_two.to_json))
85
+ documents = PersistentArticle.find [2, 3]
86
+
87
+ assert_equal 2, documents.count
88
+ end
89
+
90
+ should "find all documents" do
91
+ Configuration.client.stubs(:post).returns(mock_response(@find_all.to_json))
92
+ documents = PersistentArticle.all
93
+
94
+ assert_equal 3, documents.count
95
+ assert_equal 'First', documents.first.attributes['title']
96
+ assert_equal PersistentArticle.find(:all).map { |e| e.id }, PersistentArticle.all.map { |e| e.id }
97
+ end
98
+
99
+ should "find first document" do
100
+ Configuration.client.expects(:post).returns(mock_response(@find_first.to_json))
101
+ document = PersistentArticle.first
102
+
103
+ assert_equal 'First', document.attributes['title']
104
+ end
105
+
106
+ should "raise error when passing incorrect argument" do
107
+ assert_raise(ArgumentError) do
108
+ PersistentArticle.find :name => 'Test'
109
+ end
110
+ end
111
+
112
+ should_eventually "raise error when document is not found" do
113
+ assert_raise(DocumentNotFound) do
114
+ PersistentArticle.find 'xyz001'
115
+ end
116
+ end
117
+
118
+ end
119
+
120
+ context "Persistent model" do
121
+
122
+ setup { @article = PersistentArticle.new :title => 'Test', :tags => [:one, :two] }
123
+
124
+ context "attribute methods" do
125
+
126
+ should "allow to set attributes on initialization" do
127
+ assert_not_nil @article.attributes
128
+ assert_equal 'Test', @article.attributes['title']
129
+ end
130
+
131
+ should "allow to leave attributes blank on initialization" do
132
+ assert_nothing_raised { PersistentArticle.new }
133
+ end
134
+
135
+ should "have getter methods for attributes" do
136
+ assert_not_nil @article.title
137
+ assert_equal 'Test', @article.title
138
+ assert_equal [:one, :two], @article.tags
139
+ end
140
+
141
+ should "have getter methods for attribute passed as a String" do
142
+ article = PersistentArticle.new 'title' => 'Tony Montana'
143
+ assert_not_nil article.title
144
+ assert_equal 'Tony Montana', article.title
145
+ end
146
+
147
+ should "raise error when getting unknown attribute" do
148
+ assert_raise(NoMethodError) do
149
+ @article.krapulitz
150
+ end
151
+ end
152
+
153
+ should "not raise error when getting unset attribute" do
154
+ article = PersistentArticle.new :title => 'Test'
155
+
156
+ assert_nothing_raised { article.published_on }
157
+ assert_nil article.published_on
158
+ end
159
+
160
+ should_eventually "return default value for attribute" do
161
+ article = PersistentArticle.new :title => 'Test'
162
+ article.class_eval do
163
+ property :title
164
+ property :tags, :default => []
165
+ end
166
+
167
+ assert_nothing_raised { article.tags }
168
+ assert_equal [], article.tags
169
+ end
170
+
171
+ should "have query method for attribute" do
172
+ assert_equal true, @article.title?
173
+ end
174
+
175
+ should "raise error when querying for unknown attribute" do
176
+ assert_raise(NoMethodError) do
177
+ @article.krapulitz?
178
+ end
179
+ end
180
+
181
+ should "not raise error when querying for unset attribute" do
182
+ article = PersistentArticle.new :title => 'Test'
183
+
184
+ assert_nothing_raised { article.published_on? }
185
+ assert ! article.published_on?
186
+ end
187
+
188
+ should "return true for respond_to? calls for set attributes" do
189
+ article = PersistentArticle.new :title => 'Test'
190
+ assert article.respond_to?(:title)
191
+ end
192
+
193
+ should "return false for respond_to? calls for unknown attributes" do
194
+ article = PersistentArticle.new :title => 'Test'
195
+ assert ! article.respond_to?(:krapulitz)
196
+ end
197
+
198
+ should "return true for respond_to? calls for defined but unset attributes" do
199
+ article = PersistentArticle.new :title => 'Test'
200
+
201
+ assert article.respond_to?(:published_on)
202
+ end
203
+
204
+ should "have attribute names" do
205
+ article = PersistentArticle.new :title => 'Test', :tags => ['one', 'two']
206
+ assert_equal ['published_on', 'tags', 'title'], article.attribute_names
207
+ end
208
+
209
+ should "have setter method for attribute" do
210
+ @article.title = 'Updated'
211
+ assert_equal 'Updated', @article.title
212
+ assert_equal 'Updated', @article.attributes['title']
213
+ end
214
+
215
+ should_eventually "allow to set deeply nested attributes on initialization" do
216
+ article = PersistentArticle.new :title => 'Test', :author => { :first_name => 'John', :last_name => 'Smith' }
217
+
218
+ assert_equal 'John', article.author.first_name
219
+ assert_equal 'Smith', article.author.last_name
220
+ assert_equal({ :first_name => 'John', :last_name => 'Smith' }, article.attributes['author'])
221
+ end
222
+
223
+ should_eventually "allow to set deeply nested attributes on update" do
224
+ article = PersistentArticle.new :title => 'Test', :author => { :first_name => 'John', :last_name => 'Smith' }
225
+
226
+ article.author.first_name = 'Robert'
227
+ article.author.last_name = 'Carpenter'
228
+
229
+ assert_equal 'Robert', article.author.first_name
230
+ assert_equal 'Carpenter', article.author.last_name
231
+ assert_equal({ :first_name => 'Robert', :last_name => 'Carpenter' }, article.attributes['author'])
232
+ end
233
+
234
+ end
235
+
236
+ context "when creating" do
237
+
238
+ should "save the document with generated ID in the database" do
239
+ Configuration.client.expects(:post).with("#{Configuration.url}/persistent_articles/persistent_article/",
240
+ '{"title":"Test","tags":["one","two"],"published_on":null}').
241
+ returns(mock_response('{"ok":true,"_id":"abc123"}'))
242
+ article = PersistentArticle.create :title => 'Test', :tags => [:one, :two]
243
+
244
+ assert article.persisted?, "#{article.inspect} should be `persisted?`"
245
+ assert_equal 'abc123', article.id
246
+ end
247
+
248
+ should "save the document with custom ID in the database" do
249
+ Configuration.client.expects(:post).with("#{Configuration.url}/persistent_articles/persistent_article/r2d2",
250
+ '{"title":"Test","id":"r2d2","tags":null,"published_on":null}').
251
+ returns(mock_response('{"ok":true,"_id":"r2d2"}'))
252
+ article = PersistentArticle.create :id => 'r2d2', :title => 'Test'
253
+
254
+ assert_equal 'r2d2', article.id
255
+ end
256
+
257
+ should "perform model validations" do
258
+ Configuration.client.expects(:post).never
259
+
260
+ assert ! ValidatedModel.create(:name => nil)
261
+ end
262
+
263
+ end
264
+
265
+ context "when creating" do
266
+
267
+ should "set the id property" do
268
+ Configuration.client.expects(:post).with("#{Configuration.url}/persistent_articles/persistent_article/",
269
+ {:title => 'Test', :tags => nil, :published_on => nil}.to_json).
270
+ returns(mock_response('{"ok":true,"_id":"1"}'))
271
+
272
+ article = PersistentArticle.create :title => 'Test'
273
+ assert_equal '1', article.id
274
+ end
275
+
276
+ should "not set the id property if already set" do
277
+ Configuration.client.expects(:post).with("#{Configuration.url}/persistent_articles/persistent_article/123",
278
+ '{"title":"Test","id":"123","tags":null,"published_on":null}').
279
+ returns(mock_response('{"ok":true, "_id":"XXX"}'))
280
+
281
+ article = PersistentArticle.create :id => '123', :title => 'Test'
282
+ assert_equal '123', article.id
283
+ end
284
+
285
+ end
286
+
287
+ context "when saving" do
288
+
289
+ should "save the document with updated attribute" do
290
+ article = PersistentArticle.new :id => 1, :title => 'Test'
291
+
292
+ Configuration.client.expects(:post).with("#{Configuration.url}/persistent_articles/persistent_article/1",
293
+ '{"title":"Test","id":1,"tags":null,"published_on":null}').
294
+ returns(mock_response('{"ok":true,"_id":"1"}'))
295
+ assert article.save
296
+
297
+ article.title = 'Updated'
298
+ Configuration.client.expects(:post).with("#{Configuration.url}/persistent_articles/persistent_article/1",
299
+ '{"title":"Updated","id":1,"tags":null,"published_on":null}').
300
+ returns(mock_response('{"ok":true,"_id":"1"}'))
301
+ assert article.save
302
+ end
303
+
304
+ should "perform validations" do
305
+ article = ValidatedModel.new :name => nil
306
+ assert ! article.save
307
+ end
308
+
309
+ should "set the id property" do
310
+ article = PersistentArticle.new
311
+ article.title = 'Test'
312
+
313
+ Configuration.client.expects(:post).with("#{Configuration.url}/persistent_articles/persistent_article/",
314
+ article.to_indexed_json).
315
+ returns(mock_response('{"ok":true,"_id":"1"}'))
316
+ assert article.save
317
+ assert_equal '1', article.id
318
+ end
319
+
320
+ should "not set the id property if already set" do
321
+ article = PersistentArticle.new
322
+ article.id = '123'
323
+ article.title = 'Test'
324
+
325
+ Configuration.client.expects(:post).with("#{Configuration.url}/persistent_articles/persistent_article/123",
326
+ '{"title":"Test","id":"123","tags":null,"published_on":null}').
327
+ returns(mock_response('{"ok":true,"_id":"XXX"}'))
328
+ assert article.save
329
+ assert_equal '123', article.id
330
+ end
331
+
332
+ end
333
+
334
+ context "when destroying" do
335
+
336
+ should "delete the document from the database" do
337
+ Configuration.client.expects(:post).with("#{Configuration.url}/persistent_articles/persistent_article/123",
338
+ '{"title":"Test","id":"123","tags":null,"published_on":null}').
339
+ returns(mock_response('{"ok":true,"_id":"123"}'))
340
+ Configuration.client.expects(:delete).with("#{Configuration.url}/persistent_articles/persistent_article/123")
341
+
342
+ article = PersistentArticle.new :id => '123', :title => 'Test'
343
+ article.save
344
+ article.destroy
345
+ end
346
+
347
+ end
348
+
349
+ context "when updating attributes" do
350
+
351
+ should "update single attribute" do
352
+ @article.expects(:save).returns(true)
353
+
354
+ @article.update_attribute :title, 'Updated'
355
+ assert_equal 'Updated', @article.title
356
+ end
357
+
358
+ should "update all attributes" do
359
+ @article.expects(:save).returns(true)
360
+
361
+ @article.update_attributes :title => 'Updated', :tags => ['three']
362
+ assert_equal 'Updated', @article.title
363
+ assert_equal ['three'], @article.tags
364
+ end
365
+
366
+ end
367
+
368
+ end
369
+
370
+ context "Persistent model with mapping definition" do
371
+
372
+ should "create the index with mapping" do
373
+ expected_mapping = {
374
+ :mappings => { :persistent_article_with_mapping => {
375
+ :properties => { :title => { :type => 'string', :analyzer => 'snowball', :boost => 10 } }
376
+ }}
377
+ }
378
+
379
+ Tire::Index.any_instance.expects(:create).with(expected_mapping)
380
+
381
+ class ::PersistentArticleWithMapping
382
+
383
+ include Tire::Model::Persistence
384
+ include Tire::Model::Search
385
+ include Tire::Model::Callbacks
386
+
387
+ mapping do
388
+ property :title, :type => 'string', :analyzer => 'snowball', :boost => 10
389
+ end
390
+
391
+ end
392
+
393
+ assert_equal 'snowball', PersistentArticleWithMapping.mapping[:title][:analyzer]
394
+ end
395
+
396
+ end
397
+
398
+ end
399
+ end
400
+ end