pose 1.2.0 → 1.2.1

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.
@@ -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