elastictastic 0.5.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 (57) hide show
  1. data/LICENSE +19 -0
  2. data/README.md +326 -0
  3. data/lib/elastictastic/association.rb +21 -0
  4. data/lib/elastictastic/bulk_persistence_strategy.rb +70 -0
  5. data/lib/elastictastic/callbacks.rb +30 -0
  6. data/lib/elastictastic/child_collection_proxy.rb +56 -0
  7. data/lib/elastictastic/client.rb +101 -0
  8. data/lib/elastictastic/configuration.rb +35 -0
  9. data/lib/elastictastic/dirty.rb +130 -0
  10. data/lib/elastictastic/discrete_persistence_strategy.rb +52 -0
  11. data/lib/elastictastic/document.rb +98 -0
  12. data/lib/elastictastic/errors.rb +7 -0
  13. data/lib/elastictastic/field.rb +38 -0
  14. data/lib/elastictastic/index.rb +19 -0
  15. data/lib/elastictastic/mass_assignment_security.rb +15 -0
  16. data/lib/elastictastic/middleware.rb +119 -0
  17. data/lib/elastictastic/nested_document.rb +29 -0
  18. data/lib/elastictastic/new_relic_instrumentation.rb +26 -0
  19. data/lib/elastictastic/observer.rb +3 -0
  20. data/lib/elastictastic/observing.rb +21 -0
  21. data/lib/elastictastic/parent_child.rb +115 -0
  22. data/lib/elastictastic/persistence.rb +67 -0
  23. data/lib/elastictastic/properties.rb +236 -0
  24. data/lib/elastictastic/railtie.rb +35 -0
  25. data/lib/elastictastic/resource.rb +4 -0
  26. data/lib/elastictastic/scope.rb +283 -0
  27. data/lib/elastictastic/scope_builder.rb +32 -0
  28. data/lib/elastictastic/scoped.rb +20 -0
  29. data/lib/elastictastic/search.rb +180 -0
  30. data/lib/elastictastic/server_error.rb +15 -0
  31. data/lib/elastictastic/test_helpers.rb +172 -0
  32. data/lib/elastictastic/util.rb +63 -0
  33. data/lib/elastictastic/validations.rb +45 -0
  34. data/lib/elastictastic/version.rb +3 -0
  35. data/lib/elastictastic.rb +82 -0
  36. data/spec/environment.rb +6 -0
  37. data/spec/examples/active_model_lint_spec.rb +20 -0
  38. data/spec/examples/bulk_persistence_strategy_spec.rb +233 -0
  39. data/spec/examples/callbacks_spec.rb +96 -0
  40. data/spec/examples/dirty_spec.rb +238 -0
  41. data/spec/examples/document_spec.rb +600 -0
  42. data/spec/examples/mass_assignment_security_spec.rb +13 -0
  43. data/spec/examples/middleware_spec.rb +92 -0
  44. data/spec/examples/observing_spec.rb +141 -0
  45. data/spec/examples/parent_child_spec.rb +308 -0
  46. data/spec/examples/properties_spec.rb +92 -0
  47. data/spec/examples/scope_spec.rb +491 -0
  48. data/spec/examples/search_spec.rb +382 -0
  49. data/spec/examples/spec_helper.rb +15 -0
  50. data/spec/examples/validation_spec.rb +65 -0
  51. data/spec/models/author.rb +9 -0
  52. data/spec/models/blog.rb +5 -0
  53. data/spec/models/comment.rb +5 -0
  54. data/spec/models/post.rb +41 -0
  55. data/spec/models/post_observer.rb +11 -0
  56. data/spec/support/fakeweb_request_history.rb +13 -0
  57. metadata +227 -0
@@ -0,0 +1,233 @@
1
+ require File.expand_path('../spec_helper', __FILE__)
2
+
3
+ describe Elastictastic::BulkPersistenceStrategy do
4
+ include Elastictastic::TestHelpers
5
+
6
+ let(:last_request) { FakeWeb.last_request }
7
+ let(:bulk_requests) do
8
+ last_request.body.split("\n").map do |request|
9
+ JSON.parse(request)
10
+ end
11
+ end
12
+
13
+ describe 'create without ID' do
14
+ let(:post) { Post.new }
15
+
16
+ before do
17
+ stub_elasticsearch_bulk(
18
+ 'create' => { '_index' => 'default', '_type' => 'post', '_id' => '123', '_version' => 1, 'ok' => true }
19
+ )
20
+ Elastictastic.bulk do
21
+ post.title = 'Bulky'
22
+ post.save
23
+ end
24
+ end
25
+
26
+ it 'should send bulk request' do
27
+ last_request.path.should == '/_bulk'
28
+ end
29
+
30
+ it 'should send index operation' do
31
+ bulk_requests.should == [
32
+ { 'create' => { '_index' => 'default', '_type' => 'post' }},
33
+ post.elasticsearch_doc
34
+ ]
35
+ end
36
+
37
+ it 'should set ID' do
38
+ post.id.should == '123'
39
+ end
40
+
41
+ it 'should set persisted' do
42
+ post.should be_persisted
43
+ end
44
+
45
+ it 'should have final newline' do
46
+ last_request.body[-1].should == "\n"
47
+ end
48
+ end
49
+
50
+ describe 'before bulk operation completes' do
51
+ let(:post) { Post.new }
52
+
53
+ around do |example|
54
+ Elastictastic.bulk do
55
+ example.run
56
+ # have to do this here because the before/after hooks run inside the
57
+ # around hook
58
+ stub_elasticsearch_bulk(
59
+ 'create' => { '_index' => 'default', '_type' => 'post', '_id' => '123', '_version' => 1, 'ok' => true }
60
+ )
61
+ end
62
+ end
63
+
64
+ it 'should not set ID' do
65
+ post.save
66
+ post.id.should be_nil
67
+ end
68
+
69
+ it 'should not set persisted' do
70
+ post.should be_transient
71
+ end
72
+
73
+ it 'should not allow you to call save again on transient document' do
74
+ post.save
75
+ expect { post.save }.to raise_error(Elastictastic::OperationNotAllowed)
76
+ end
77
+ end
78
+
79
+ describe 'creating multiple' do
80
+ let(:posts) { Array.new(2) { Post.new }}
81
+
82
+ before do
83
+ stub_elasticsearch_bulk(
84
+ { 'create' => { '_index' => 'default', '_type' => 'post', '_id' => '123', '_version' => 1, 'ok' => true }},
85
+ { 'create' => { '_index' => 'default', '_type' => 'post', '_id' => '124', '_version' => 1, 'ok' => true }}
86
+ )
87
+ Elastictastic.bulk { posts.each { |post| post.save }}
88
+ end
89
+
90
+ it 'should send both create operations' do
91
+ bulk_requests.should == posts.map do |post|
92
+ [
93
+ { 'create' => { '_index' => 'default', '_type' => 'post' }},
94
+ post.elasticsearch_doc
95
+ ]
96
+ end.flatten
97
+ end
98
+
99
+ it 'should set IDs' do
100
+ posts.map { |post| post.id }.should == %w(123 124)
101
+ end
102
+ end
103
+
104
+ describe 'create with ID set' do
105
+ let(:post) do
106
+ Post.new.tap do |post|
107
+ post.id = '123'
108
+ post.title = 'bulky'
109
+ end
110
+ end
111
+
112
+ before do
113
+ stub_elasticsearch_bulk(
114
+ 'create' => { '_index' => 'default', '_type' => 'post', '_id' => '123', '_version' => 1, 'ok' => true }
115
+ )
116
+ Elastictastic.bulk { post.save }
117
+ end
118
+
119
+ it 'should send ID in request to create' do
120
+ bulk_requests.should == [
121
+ { 'create' => { '_index' => 'default', '_type' => 'post', '_id' => '123' }},
122
+ post.elasticsearch_doc
123
+ ]
124
+ end
125
+
126
+ it 'should set object persistent' do
127
+ post.should be_persisted
128
+ end
129
+
130
+ it 'should retain ID' do
131
+ post.id.should == '123'
132
+ end
133
+ end
134
+
135
+ describe '#update' do
136
+ let(:post) do
137
+ Post.new.tap do |post|
138
+ post.id = '123'
139
+ post.persisted!
140
+ end
141
+ end
142
+
143
+ before do
144
+ stub_elasticsearch_bulk(
145
+ 'index' => { '_index' => 'default', '_type' => 'post', '_id' => '123', '_version' => 1, 'ok' => true }
146
+ )
147
+ end
148
+
149
+ it 'should send update' do
150
+ Elastictastic.bulk { post.save }
151
+
152
+ bulk_requests.should == [
153
+ { 'index' => { '_index' => 'default', '_type' => 'post', '_id' => '123' }},
154
+ post.elasticsearch_doc
155
+ ]
156
+ end
157
+ end
158
+
159
+ describe 'destroy' do
160
+ let(:post) do
161
+ Post.new.tap do |post|
162
+ post.id = '123'
163
+ post.title = 'bulky'
164
+ post.persisted!
165
+ end
166
+ end
167
+
168
+ before do
169
+ stub_elasticsearch_bulk(
170
+ 'destroy' => { '_index' => 'default', '_type' => 'post', '_id' => '123', '_version' => 1, 'ok' => true }
171
+ )
172
+ Elastictastic.bulk { post.destroy }
173
+ end
174
+
175
+ it 'should send destroy' do
176
+ bulk_requests.should == [
177
+ { 'delete' => { '_index' => 'default', '_type' => 'post', '_id' => '123' }}
178
+ ]
179
+ end
180
+
181
+ it 'should mark record as not persistent' do
182
+ post.should_not be_persisted
183
+ end
184
+ end
185
+
186
+ shared_examples_for 'block with error' do
187
+ it 'should not run bulk operation' do
188
+ error_proc.call rescue nil
189
+ last_request.should_not be
190
+ end
191
+
192
+ it 'should return to individual persistence strategy' do
193
+ error_proc.call rescue nil
194
+ stub_elasticsearch_create('default', 'post')
195
+ Post.new.save
196
+ last_request.path.should == '/default/post'
197
+ end
198
+ end
199
+
200
+ describe 'with uncaught exception raised' do
201
+ let :error_proc do
202
+ lambda do
203
+ Elastictastic.bulk do
204
+ Post.new.save
205
+ raise
206
+ end
207
+ end
208
+ end
209
+
210
+ it 'should propagate error up' do
211
+ expect(&error_proc).to raise_error(RuntimeError)
212
+ end
213
+
214
+ it_should_behave_like 'block with error'
215
+ end
216
+
217
+ describe 'raising CancelBulkOperation' do
218
+ let :error_proc do
219
+ lambda do
220
+ Elastictastic.bulk do
221
+ Post.new.save
222
+ raise Elastictastic::CancelBulkOperation
223
+ end
224
+ end
225
+ end
226
+
227
+ it 'should not propagate error' do
228
+ expect(&error_proc).not_to raise_error
229
+ end
230
+
231
+ it_should_behave_like 'block with error'
232
+ end
233
+ end
@@ -0,0 +1,96 @@
1
+ require File.expand_path('../spec_helper', __FILE__)
2
+
3
+ describe Elastictastic::Callbacks do
4
+ include Elastictastic::TestHelpers
5
+
6
+ let(:MyModel) do
7
+ Class.new do
8
+ def self.name
9
+ 'MyModel'
10
+ end
11
+
12
+ include Elastictastic::Document
13
+
14
+ before_save :before_save_ran!
15
+ before_create :before_create_ran!
16
+ before_update :before_update_ran!
17
+ before_destroy :before_destroy_ran!
18
+
19
+ def hooks_that_ran
20
+ @hooks_that_ran ||= Set[]
21
+ end
22
+
23
+ def has_run_hook?(hook)
24
+ hooks_that_ran.include?(hook.to_sym)
25
+ end
26
+
27
+ private
28
+
29
+ def method_missing(method, *args, &block)
30
+ if method.to_s =~ /^(.*)_ran!$/
31
+ hooks_that_ran << $1.to_sym
32
+ else
33
+ super
34
+ end
35
+ end
36
+ end
37
+ end
38
+
39
+ let(:id) { '123' }
40
+ let(:instance) { MyModel().new }
41
+ let(:persisted_instance) do
42
+ MyModel().new.tap do |instance|
43
+ instance.elasticsearch_hit = { '_id' => id, '_index' => 'default' }
44
+ end
45
+ end
46
+
47
+ before do
48
+ stub_elasticsearch_create('default', 'my_model')
49
+ stub_elasticsearch_update('default', 'my_model', id)
50
+ stub_elasticsearch_destroy('default', 'my_model', id)
51
+ end
52
+
53
+ describe '#before_save' do
54
+
55
+ it 'should run before create' do
56
+ instance.save
57
+ instance.should have_run_hook(:before_save)
58
+ end
59
+
60
+ it 'should run before update' do
61
+ persisted_instance.save
62
+ persisted_instance.should have_run_hook(:before_save)
63
+ end
64
+ end
65
+
66
+ describe '#before_create' do
67
+ it 'should run before create' do
68
+ instance.save
69
+ instance.should have_run_hook(:before_create)
70
+ end
71
+
72
+ it 'should run before update' do
73
+ persisted_instance.save
74
+ persisted_instance.should_not have_run_hook(:before_create)
75
+ end
76
+ end
77
+
78
+ describe '#before_update' do
79
+ it 'should not run before create' do
80
+ instance.save
81
+ instance.should_not have_run_hook :before_update
82
+ end
83
+
84
+ it 'should run before update' do
85
+ persisted_instance.save
86
+ persisted_instance.should have_run_hook(:before_update)
87
+ end
88
+ end
89
+
90
+ describe '#before_destroy' do
91
+ it 'should run before destroy' do
92
+ persisted_instance.destroy
93
+ persisted_instance.should have_run_hook(:before_destroy)
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,238 @@
1
+ require File.expand_path('../spec_helper', __FILE__)
2
+
3
+ describe Elastictastic::Dirty do
4
+ include Elastictastic::TestHelpers
5
+
6
+ let(:post) do
7
+ Post.new.tap do |post|
8
+ post.elasticsearch_hit = {
9
+ '_id' => '1',
10
+ '_type' => 'post',
11
+ '_index' => 'default',
12
+ '_source' => source
13
+ }
14
+ end
15
+ end
16
+
17
+ before do
18
+ stub_elasticsearch_update('default', 'post', '1')
19
+ end
20
+
21
+ context 'top-level attribute' do
22
+ let(:source) { { 'title' => 'first title' } }
23
+
24
+ context 'with no attribute changed' do
25
+ it 'should not be changed' do
26
+ post.should_not be_changed
27
+ end
28
+
29
+ it 'should not have any changes' do
30
+ post.changes.should be_empty
31
+ end
32
+
33
+ it 'should not have the title attribute changed' do
34
+ post.should_not be_title_changed
35
+ end
36
+ end
37
+
38
+ context 'with title attribute changed' do
39
+ before do
40
+ post.title = 'hey guy'
41
+ end
42
+
43
+ it 'should be changed' do
44
+ post.should be_changed
45
+ end
46
+
47
+ it 'should expose change' do
48
+ post.changes[:title].should == ['first title', 'hey guy']
49
+ end
50
+
51
+ it 'should have the title attribute changed' do
52
+ post.should be_title_changed
53
+ end
54
+ end
55
+
56
+ context 'after change and save' do
57
+ before do
58
+ post.title = 'hey guy'
59
+ post.save
60
+ end
61
+
62
+ it 'should not be changed' do
63
+ post.should_not be_changed
64
+ end
65
+
66
+ it 'should not have any changes' do
67
+ post.changes.should be_empty
68
+ end
69
+
70
+ it 'should not have the title attribute changed' do
71
+ post.should_not be_title_changed
72
+ end
73
+ end
74
+ end
75
+
76
+ context 'single nested document' do
77
+ let(:source) { { 'author' => { 'name' => 'Mat Brown' }} }
78
+
79
+ context 'with nothing changed' do
80
+ it 'should not be changed' do
81
+ post.should_not be_changed
82
+ end
83
+ end
84
+
85
+ context 'with nested document attribute changed' do
86
+ before do
87
+ post.author.name = 'Barack Obama'
88
+ end
89
+
90
+ it 'should be changed' do
91
+ post.should be_changed
92
+ end
93
+
94
+ it 'should mark association as changed' do
95
+ post.changed.should == %w(author)
96
+ end
97
+
98
+ it 'should return *_changed? for the association' do
99
+ post.should be_author_changed
100
+ end
101
+
102
+ it 'should have change' do
103
+ change = post.changes['author']
104
+ change.map { |author| author.name }.should == ['Mat Brown', 'Barack Obama']
105
+ end
106
+
107
+ it 'should not be changed after save' do
108
+ post.save
109
+ post.should_not be_changed
110
+ end
111
+
112
+ it 'should not have changed nested document after save' do
113
+ post.save
114
+ post.author.should_not be_changed
115
+ end
116
+ end
117
+
118
+ context 'with nested document replaced' do
119
+ before do
120
+ post.author = Author.new(:name => 'Barack Obama')
121
+ end
122
+
123
+ it 'should be changed' do
124
+ post.should be_changed
125
+ end
126
+
127
+ it 'should expose changes' do
128
+ post.changes['author'].map { |author| author.name }.should ==
129
+ ['Mat Brown', 'Barack Obama']
130
+ end
131
+ end
132
+ end
133
+
134
+ context 'nested document collection' do
135
+ let(:source) { { 'comments' => [{ 'body' => 'I like pizza' }] } }
136
+
137
+ let(:mapped_changes) do
138
+ post.changes['comments'].map do |change|
139
+ change.map { |comment| comment.body }
140
+ end
141
+ end
142
+
143
+ context 'with nothing changed' do
144
+ it 'should not be changed' do
145
+ post.should_not be_changed
146
+ end
147
+ end
148
+
149
+ context 'when document changed' do
150
+ before do
151
+ post.comments.first.body = 'I like lasagne'
152
+ end
153
+
154
+ it 'should be changed' do
155
+ post.should be_changed
156
+ end
157
+
158
+ it 'should expose changed document' do
159
+ mapped_changes.should == [['I like pizza'], ['I like lasagne']]
160
+ end
161
+
162
+ it 'should not be changed after save' do
163
+ post.save
164
+ post.should_not be_changed
165
+ end
166
+
167
+ it 'should not have changed nested document after save' do
168
+ post.save
169
+ post.comments.first.should_not be_changed
170
+ end
171
+ end
172
+
173
+ context 'when document added' do
174
+ before do
175
+ post.comments << Comment.new(:body => 'Lasagne is better')
176
+ end
177
+
178
+ it 'should be changed' do
179
+ post.should be_changed
180
+ end
181
+
182
+ it 'should expose new document' do
183
+ mapped_changes.should == [
184
+ ['I like pizza'],
185
+ ['I like pizza', 'Lasagne is better']
186
+ ]
187
+ end
188
+
189
+ it 'should not be changed after save' do
190
+ post.save
191
+ post.should_not be_changed
192
+ end
193
+
194
+ it 'should not have changed nested documents after save' do
195
+ post.save
196
+ post.comments.each { |comment| comment.should_not be_changed }
197
+ end
198
+ end
199
+
200
+ context 'when document removed' do
201
+ before do
202
+ post.comments.delete(post.comments.first)
203
+ end
204
+
205
+ it 'should be changed' do
206
+ post.should be_changed
207
+ end
208
+
209
+ it 'should show changes' do
210
+ mapped_changes.should == [['I like pizza'], []]
211
+ end
212
+
213
+ it 'should not be changed after save' do
214
+ post.save
215
+ post.should_not be_changed
216
+ end
217
+ end
218
+
219
+ context 'with collection replaced' do
220
+ before do
221
+ post.comments = [Comment.new(:body => 'I like lasagne')]
222
+ end
223
+
224
+ it 'should be changed' do
225
+ post.should be_changed
226
+ end
227
+
228
+ it 'should expose changes' do
229
+ mapped_changes.should == [['I like pizza'], ['I like lasagne']]
230
+ end
231
+
232
+ it 'should not be changed after save' do
233
+ post.save
234
+ post.should_not be_changed
235
+ end
236
+ end
237
+ end
238
+ end