pose 1.2.0 → 1.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,20 @@
1
+ # Verifies that a posable object has the given pose words in its search index.
2
+ RSpec::Matchers.define :have_pose_words do |expected|
3
+
4
+ match do |actual|
5
+ actual.should have(expected.size).pose_words
6
+ texts = actual.pose_words.map &:text
7
+ expected.each do |expected_word|
8
+ # Note (KG): Can't use text.should include(expected_word) here
9
+ # because Ruby thinks I want to include a Module for some reason.
10
+ texts.include?(expected_word).should be_true
11
+ end
12
+ end
13
+
14
+ failure_message_for_should do |actual|
15
+ texts = actual.pose_words.map &:text
16
+ "expected that subject would have pose words [#{expected.join ', '}], but it has [#{texts.join ', '}]"
17
+ end
18
+
19
+ end
20
+
@@ -0,0 +1,8 @@
1
+ class PosableOne < ActiveRecord::Base
2
+ posify { text }
3
+ end
4
+
5
+ class PosableTwo < ActiveRecord::Base
6
+ posify { text }
7
+ end
8
+
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pose
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.0
4
+ version: 1.2.1
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-09-30 00:00:00.000000000 Z
12
+ date: 2012-10-06 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rails
@@ -75,6 +75,22 @@ dependencies:
75
75
  - - ! '>='
76
76
  - !ruby/object:Gem::Version
77
77
  version: '0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: database_cleaner
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
78
94
  - !ruby/object:Gem::Dependency
79
95
  name: factory_girl
80
96
  requirement: !ruby/object:Gem::Requirement
@@ -183,12 +199,13 @@ files:
183
199
  - doc/top-level-namespace.html
184
200
  - lib/generators/pose_generator.rb
185
201
  - lib/generators/templates/migration.rb
186
- - lib/pose/base_additions.rb
202
+ - lib/pose/activerecord_base_additions.rb
203
+ - lib/pose/internal_helpers.rb
187
204
  - lib/pose/model_additions.rb
188
205
  - lib/pose/models/pose_assignment.rb
189
206
  - lib/pose/models/pose_word.rb
190
207
  - lib/pose/railtie.rb
191
- - lib/pose/static_helpers.rb
208
+ - lib/pose/static_api.rb
192
209
  - lib/pose/version.rb
193
210
  - lib/pose.rb
194
211
  - lib/tasks/pose_tasks.rake
@@ -196,10 +213,13 @@ files:
196
213
  - Rakefile
197
214
  - README.md
198
215
  - spec/factories.rb
216
+ - spec/internal_helpers_spec.rb
217
+ - spec/pose_api_spec.rb
199
218
  - spec/pose_assignment_spec.rb
200
- - spec/pose_spec.rb
201
219
  - spec/pose_word_spec.rb
202
220
  - spec/spec_helper.rb
221
+ - spec/support/matchers.rb
222
+ - spec/support/models.rb
203
223
  homepage: http://github.com/kevgo/pose
204
224
  licenses: []
205
225
  post_install_message:
@@ -226,7 +246,10 @@ specification_version: 3
226
246
  summary: A polymorphic, storage-system independent search engine for Ruby on Rails.
227
247
  test_files:
228
248
  - spec/factories.rb
249
+ - spec/internal_helpers_spec.rb
250
+ - spec/pose_api_spec.rb
229
251
  - spec/pose_assignment_spec.rb
230
- - spec/pose_spec.rb
231
252
  - spec/pose_word_spec.rb
232
253
  - spec/spec_helper.rb
254
+ - spec/support/matchers.rb
255
+ - spec/support/models.rb
@@ -1,173 +0,0 @@
1
- # Static helper methods of the Pose gem.
2
- module Pose
3
- extend ActiveSupport::Concern
4
-
5
- # By default, doesn't run in tests.
6
- # Set this to true to test the search functionality.
7
- CONFIGURATION = { search_in_tests: false }
8
-
9
- class <<self
10
-
11
- # Returns whether Pose is configured to perform search.
12
- # This setting exists to disable search in tests.
13
- #
14
- # @return [false, true]
15
- def perform_search?
16
- !(Rails.env == 'test' and !CONFIGURATION[:search_in_tests])
17
- end
18
-
19
- # Returns all words that begin with the given query string.
20
- # This can be used for autocompletion functionality.
21
- #
22
- # @param [String]
23
- # @return [Array<String>]
24
- def autocomplete_words query
25
- return [] if query.blank?
26
- PoseWord.where('text LIKE ?', "#{Pose.root_word(query)[0]}%").map(&:text)
27
- end
28
-
29
- # Returns all strings that are in new_words, but not in existing_words.
30
- # Helper method.
31
- #
32
- # @param [Array<String>] existing_words The words that are already associated with the object.
33
- # @param [Array<String>] new_words The words thet the object should have from now on.
34
- #
35
- # @return [Array<String>] The words that need to be added to the existing_words array.
36
- def get_words_to_add existing_words, new_words
37
- new_words - existing_words.map(&:text)
38
- end
39
-
40
- # Helper method.
41
- # Returns the id of all word objects that are in existing_words, but not in new_words.
42
- #
43
- # @param [Array<String>] existing_words The words that are already associated with the object.
44
- # @param [Array<String>] new_words The words thet the object should have from now on.
45
- #
46
- # @return [Array<String>] The words that need to be removed from the existing_words array.
47
- def get_words_to_remove existing_words, new_words
48
- existing_words.map do |existing_word|
49
- existing_word unless new_words.include?(existing_word.text)
50
- end.compact
51
- end
52
-
53
- # Returns whether the given string is a URL.
54
- #
55
- # @param [String] word The string to check.
56
- #
57
- # @return [Boolean]
58
- def is_url? word
59
- URI::parse(word).scheme == 'http'
60
- rescue URI::InvalidURIError
61
- false
62
- end
63
-
64
- # Simplifies the given word to a generic search form.
65
- #
66
- # @param [String] raw_word The word to make searchable.
67
- #
68
- # @return [String] The stemmed version of the word.
69
- def root_word raw_word
70
- result = []
71
- raw_word_copy = raw_word[0..-1]
72
- raw_word_copy.gsub! '%20', ' '
73
- raw_word_copy.gsub! /[()*<>'",;\?\-\=&%#]/, ' '
74
- raw_word_copy.gsub! /\s+/, ' '
75
- raw_word_copy.split(' ').each do |word|
76
- if Pose.is_url?(word)
77
- result.concat word.split(/[\.\/\:]/).delete_if(&:blank?)
78
- else
79
- word.gsub! /[\-\/\._:]/, ' '
80
- word.gsub! /\s+/, ' '
81
- word.split(' ').each do |w|
82
- stemmed_word = w.parameterize.singularize
83
- result.concat stemmed_word.split ' '
84
- end
85
- end
86
- end
87
- result.uniq
88
- end
89
-
90
- # Returns all objects matching the given query.
91
- #
92
- # @param [String] query
93
- # @param (Class|[Array<Class>]) classes
94
- # @param [Hash?] options Additional options.
95
- #
96
- # @return [Hash<Class, ActiveRecord::Relation>]
97
- def search query, classes, options = {}
98
-
99
- # Turn 'classes' into an array.
100
- classes = [classes].flatten
101
- classes_names = classes.map &:name
102
- classes_names = classes_names[0] if classes_names.size == 1
103
-
104
- # Get the ids of the results.
105
- result_classes_and_ids = {}
106
- query_words = query.split(' ').map{|query_word| Pose.root_word query_word}.flatten
107
- query_words.each do |query_word|
108
- current_word_classes_and_ids = {}
109
- classes.each { |clazz| current_word_classes_and_ids[clazz.name] = [] }
110
- query = PoseAssignment.joins(:pose_word) \
111
- .select('pose_assignments.posable_id, pose_assignments.posable_type') \
112
- .where('pose_words.text LIKE ?', "#{query_word}%") \
113
- .where('posable_type IN (?)', classes_names)
114
- PoseAssignment.connection.select_all(query.to_sql).each do |pose_assignment|
115
- current_word_classes_and_ids[pose_assignment['posable_type']] << pose_assignment['posable_id'].to_i
116
- end
117
- # This is the old ActiveRecord way. Removed for performance reasons.
118
- # query.each do |pose_assignment|
119
- # current_word_classes_and_ids[pose_assignment.posable_type] << pose_assignment.posable_id
120
- # end
121
-
122
- current_word_classes_and_ids.each do |class_name, ids|
123
- if result_classes_and_ids.has_key? class_name
124
- result_classes_and_ids[class_name] = result_classes_and_ids[class_name] & ids
125
- else
126
- result_classes_and_ids[class_name] = ids
127
- end
128
- end
129
- end
130
-
131
- # Load the results by id.
132
- {}.tap do |result|
133
- result_classes_and_ids.each do |class_name, ids|
134
- result_class = Kernel.const_get class_name
135
-
136
- if ids.size == 0
137
- # Handle no results.
138
- result[result_class] = []
139
-
140
- else
141
- # Here we have results.
142
-
143
- # Limit.
144
- ids = ids.slice(0, options[:limit]) if options[:limit]
145
-
146
- if options[:result_type] == :ids
147
- # Ids requested for result.
148
-
149
- if options.has_key? :scope
150
- # We have a scope.
151
- options[:scope].each do |scope|
152
- result[result_class] = result_class.select('id').where(scope).map(&:id)
153
- end
154
- else
155
- result[result_class] = ids
156
- end
157
-
158
- else
159
- # Classes requested for result.
160
-
161
- result[result_class] = result_class.where(id: ids)
162
- if options.has_key? :scope
163
- options[:scope].each do |scope|
164
- result[result_class] = result[result_class].where(scope)
165
- end
166
- end
167
- end
168
- end
169
- end
170
- end
171
- end
172
- end
173
- end
data/spec/pose_spec.rb DELETED
@@ -1,401 +0,0 @@
1
- # encoding: utf-8
2
-
3
- require "spec_helper"
4
-
5
- ActiveRecord::Base.establish_connection adapter: 'postgresql', database: 'pose_test', min_messages: 'WARNING'
6
-
7
- describe Pose do
8
- subject { PosableOne.new }
9
-
10
- before :each do
11
- PosableOne.delete_all
12
- PosableTwo.delete_all
13
- PoseAssignment.delete_all
14
- PoseWord.delete_all
15
- end
16
-
17
- describe 'associations' do
18
- it 'allows to access the associated words of a posable object directly' do
19
- subject.should have(0).pose_words
20
- subject.pose_words << PoseWord.new(text: 'one')
21
- subject.should have_pose_words(['one'])
22
- end
23
- end
24
-
25
- describe 'update_pose_index' do
26
-
27
- context "in the 'test' environment" do
28
- after :each do
29
- Pose::CONFIGURATION[:search_in_tests] = true
30
- end
31
-
32
- it "doesn't calls update_pose_words in tests if the test flag is not enabled" do
33
- Pose::CONFIGURATION[:search_in_tests] = false
34
- subject.should_not_receive :update_pose_words
35
- subject.update_pose_index
36
- end
37
-
38
- it "calls update_pose_words in tests if the test flag is enabled" do
39
- Pose::CONFIGURATION[:search_in_tests] = true
40
- subject.should_receive :update_pose_words
41
- subject.update_pose_index
42
- end
43
- end
44
-
45
- context "in the 'production' environment' do" do
46
- before :each do
47
- @old_env = Rails.env
48
- Rails.env = 'production'
49
- end
50
-
51
- after :each do
52
- Rails.env = @old_env
53
- end
54
-
55
- it "calls update_pose_words" do
56
- subject.should_receive :update_pose_words
57
- subject.update_pose_index
58
- end
59
- end
60
- end
61
-
62
- describe 'update_pose_words' do
63
-
64
- it 'saves the words for search' do
65
- subject.text = 'foo bar'
66
- subject.update_pose_words
67
- subject.should have(2).pose_words
68
- subject.should have_pose_words ['foo', 'bar']
69
- end
70
-
71
- it 'updates the search index when the text is changed' do
72
- subject.text = 'foo'
73
- subject.save!
74
-
75
- subject.text = 'other text'
76
- subject.update_pose_words
77
-
78
- subject.should have_pose_words ['other', 'text']
79
- end
80
-
81
- it "doesn't create duplicate words" do
82
- subject.text = 'foo foo'
83
- subject.save!
84
- subject.should have(1).pose_words
85
- end
86
- end
87
-
88
- describe 'get_words_to_remove' do
89
-
90
- it "returns an array of word objects that need to be removed" do
91
- word1 = PoseWord.new text: 'one'
92
- word2 = PoseWord.new text: 'two'
93
- existing_words = [word1, word2]
94
- new_words = ['one', 'three']
95
-
96
- result = Pose.get_words_to_remove existing_words, new_words
97
-
98
- result.should eql([word2])
99
- end
100
-
101
- it 'returns an empty array if there are no words to be removed' do
102
- word1 = PoseWord.new text: 'one'
103
- word2 = PoseWord.new text: 'two'
104
- existing_words = [word1, word2]
105
- new_words = ['one', 'two']
106
-
107
- result = Pose.get_words_to_remove existing_words, new_words
108
-
109
- result.should eql([])
110
- end
111
- end
112
-
113
- describe 'get_words_to_add' do
114
-
115
- it 'returns an array with strings that need to be added' do
116
- word1 = PoseWord.new text: 'one'
117
- word2 = PoseWord.new text: 'two'
118
- existing_words = [word1, word2]
119
- new_words = ['one', 'three']
120
-
121
- result = Pose.get_words_to_add existing_words, new_words
122
-
123
- result.should eql(['three'])
124
- end
125
-
126
- it 'returns an empty array if there is nothing to be added' do
127
- word1 = PoseWord.new text: 'one'
128
- word2 = PoseWord.new text: 'two'
129
- existing_words = [word1, word2]
130
- new_words = ['one', 'two']
131
-
132
- result = Pose.get_words_to_add existing_words, new_words
133
-
134
- result.should eql([])
135
- end
136
- end
137
-
138
- describe 'root_word' do
139
-
140
- it 'converts words into singular' do
141
- Pose.root_word('bars').should eql(['bar'])
142
- end
143
-
144
- it 'removes special characters' do
145
- Pose.root_word('(bar').should eql(['bar'])
146
- Pose.root_word('bar)').should eql(['bar'])
147
- Pose.root_word('(bar)').should eql(['bar'])
148
- Pose.root_word('>foo').should eql(['foo'])
149
- Pose.root_word('<foo').should eql(['foo'])
150
- Pose.root_word('"foo"').should eql(['foo'])
151
- Pose.root_word('"foo').should eql(['foo'])
152
- Pose.root_word("'foo'").should eql(['foo'])
153
- Pose.root_word("'foo's").should eql(['foo'])
154
- Pose.root_word("foo?").should eql(['foo'])
155
- Pose.root_word("foo!").should eql(['foo'])
156
- Pose.root_word("foo/bar").should eql(['foo', 'bar'])
157
- Pose.root_word("foo-bar").should eql(['foo', 'bar'])
158
- Pose.root_word("foo--bar").should eql(['foo', 'bar'])
159
- Pose.root_word("foo.bar").should eql(['foo', 'bar'])
160
- end
161
-
162
- it 'removes umlauts' do
163
- Pose.root_word('fünf').should eql(['funf'])
164
- end
165
-
166
- it 'splits up numbers' do
167
- Pose.root_word('11.2.2011').should eql(['11', '2', '2011'])
168
- Pose.root_word('11-2-2011').should eql(['11', '2', '2011'])
169
- Pose.root_word('30:4-5').should eql(['30', '4', '5'])
170
- end
171
-
172
- it 'converts into lowercase' do
173
- Pose.root_word('London').should eql(['london'])
174
- end
175
-
176
- it "stores single-letter words" do
177
- Pose.root_word('a b').should eql(['a', 'b'])
178
- end
179
-
180
- it "does't encode external URLs" do
181
- Pose.root_word('http://web.com').should eql(['http', 'web', 'com'])
182
- end
183
-
184
- it "doesn't store empty words" do
185
- Pose.root_word(' one two ').should eql(['one', 'two'])
186
- end
187
-
188
- it "removes duplicates" do
189
- Pose.root_word('one_one').should eql(['one'])
190
- Pose.root_word('one one').should eql(['one'])
191
- end
192
-
193
- it "splits up complex URLs" do
194
- Pose.root_word('books?id=p7uyWPcVGZsC&dq=closure%20definitive%20guide&pg=PP1#v=onepage&q&f=false').should eql([
195
- "book", "id", "p7uywpcvgzsc", "dq", "closure", "definitive", "guide", "pg", "pp1", "v", "onepage", "q", "f", "false"])
196
- end
197
- end
198
-
199
- describe 'search' do
200
-
201
- it 'works' do
202
- pos1 = PosableOne.create text: 'one'
203
-
204
- result = Pose.search 'one', PosableOne
205
-
206
- result.should have(1).items
207
- result[PosableOne].should have(1).items
208
- result[PosableOne][0].should == pos1
209
- end
210
-
211
- describe 'classes parameter' do
212
- it 'returns all different classes by default' do
213
- pos1 = PosableOne.create text: 'foo'
214
- pos2 = PosableTwo.create text: 'foo'
215
-
216
- result = Pose.search 'foo', [PosableOne, PosableTwo]
217
-
218
- result.should have(2).items
219
- result[PosableOne].should == [pos1]
220
- result[PosableTwo].should == [pos2]
221
- end
222
-
223
- it 'allows to provide different classes to return' do
224
- pos1 = PosableOne.create text: 'foo'
225
- pos2 = PosableTwo.create text: 'foo'
226
-
227
- result = Pose.search 'foo', [PosableOne, PosableTwo]
228
-
229
- result.should have(2).items
230
- result[PosableOne].should == [pos1]
231
- result[PosableTwo].should == [pos2]
232
- end
233
-
234
- it 'returns only instances of the given classes' do
235
- pos1 = PosableOne.create text: 'one'
236
- pos2 = PosableTwo.create text: 'one'
237
-
238
- result = Pose.search 'one', PosableOne
239
-
240
- result.should have(1).items
241
- result[PosableOne].should == [pos1]
242
- end
243
- end
244
-
245
- describe 'query parameter' do
246
-
247
- it 'returns an empty array if nothing matches' do
248
- pos1 = PosableOne.create text: 'one'
249
-
250
- result = Pose.search 'two', PosableOne
251
-
252
- result.should == { PosableOne => [] }
253
- end
254
-
255
- it 'returns only objects that match all given query words' do
256
- pos1 = PosableOne.create text: 'one two'
257
- pos2 = PosableOne.create text: 'one three'
258
- pos3 = PosableOne.create text: 'two three'
259
-
260
- result = Pose.search 'two one', PosableOne
261
-
262
- result.should have(1).items
263
- result[PosableOne].should == [pos1]
264
- end
265
-
266
- it 'returns nothing if searching for a non-existing word' do
267
- pos1 = PosableOne.create text: 'one two'
268
-
269
- result = Pose.search 'one zonk', PosableOne
270
-
271
- result.should have(1).items
272
- result[PosableOne].should == []
273
- end
274
-
275
- it 'works if the query is given in uppercase' do
276
- pos1 = PosableOne.create text: 'one two'
277
-
278
- result = Pose.search 'OnE TwO', PosableOne
279
-
280
- result.should have(1).items
281
- result[PosableOne].should == [pos1]
282
- end
283
- end
284
-
285
- describe "'limit' parameter" do
286
-
287
- it 'works' do
288
- FactoryGirl.create :posable_one, text: 'foo one'
289
- FactoryGirl.create :posable_one, text: 'foo two'
290
- FactoryGirl.create :posable_one, text: 'foo three'
291
- FactoryGirl.create :posable_one, text: 'foo four'
292
-
293
- result = Pose.search 'foo', PosableOne, limit: 3
294
-
295
- result[PosableOne].should have(3).items
296
- end
297
- end
298
-
299
- describe "'result_type' parameter" do
300
-
301
- before :each do
302
- @foo_one = FactoryGirl.create :posable_one, text: 'foo one'
303
- end
304
-
305
- describe 'default behavior' do
306
- it 'returns full objects' do
307
- result = Pose.search 'foo', PosableOne
308
- result[PosableOne][0].should == @foo_one
309
- end
310
- end
311
-
312
- context ':ids given' do
313
- it 'returns ids instead of objects' do
314
- result = Pose.search 'foo', PosableOne, result_type: :ids
315
- result[PosableOne][0].should == @foo_one.id
316
- end
317
- end
318
- end
319
-
320
- describe "'scopes' parameter" do
321
-
322
- before :each do
323
- @one = FactoryGirl.create :posable_one, text: 'foo one', private: true
324
- @two = FactoryGirl.create :posable_one, text: 'foo two', private: false
325
- end
326
-
327
- context 'with result type :classes' do
328
-
329
- it 'limits the result set by the given scope' do
330
- result = Pose.search 'foo', PosableOne, scope: [ private: true ]
331
- result[PosableOne].should have(1).item
332
- result[PosableOne].should include @one
333
- end
334
-
335
- it 'allows to use the hash syntax for queries' do
336
- result = Pose.search 'foo', PosableOne, scope: [ private: true ]
337
- result[PosableOne].should have(1).item
338
- result[PosableOne].should include @one
339
- end
340
-
341
- it 'allows to use the string syntax for queries' do
342
- result = Pose.search 'foo', PosableOne, scope: [ ['private = ?', true] ]
343
- result[PosableOne].should have(1).item
344
- result[PosableOne].should include @one
345
- end
346
-
347
- it 'allows to combine several scopes' do
348
- @three = FactoryGirl.create :posable_one, text: 'foo two', private: true
349
- result = Pose.search 'foo', PosableOne, scope: [ {private: true}, ['text = ?', 'foo two'] ]
350
- result[PosableOne].should have(1).item
351
- result[PosableOne].should include @three
352
- end
353
- end
354
-
355
- context 'with result type :ids' do
356
-
357
- it 'limits the result set by the given scope' do
358
- result = Pose.search 'foo', PosableOne, result_type: :ids, scope: [ private: true ]
359
- result[PosableOne].should have(1).item
360
- result[PosableOne].should include @one.id
361
- end
362
- end
363
- end
364
- end
365
-
366
- describe 'autocomplete_words' do
367
-
368
- it 'returns words that start with the given phrase' do
369
- PosableOne.create text: 'great green pine tree'
370
-
371
- result = Pose.autocomplete_words 'gr'
372
-
373
- result.should have(2).words
374
- result.should include('great')
375
- result.should include('green')
376
- end
377
-
378
- it 'returns words that match the given phrase exactly' do
379
- PoseWord.create text: 'cat'
380
-
381
- result = Pose.autocomplete_words 'cat'
382
-
383
- result.should == ['cat']
384
- end
385
-
386
- it 'stems the search query' do
387
- PosableOne.create text: 'car'
388
-
389
- result = Pose.autocomplete_words 'cars'
390
-
391
- result.should have(1).words
392
- result[0].should == 'car'
393
- end
394
-
395
- it 'returns nothing if the search query is empty' do
396
- PosableOne.create text: 'foo bar'
397
- result = Pose.autocomplete_words ''
398
- result.should have(0).words
399
- end
400
- end
401
- end