elastictastic 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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