textacular 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,78 @@
1
+ $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/../lib')
2
+
3
+ require 'yaml'
4
+ require 'textacular'
5
+ require 'shoulda'
6
+ require 'pry'
7
+ require 'active_record'
8
+ require 'textacular'
9
+ require 'textacular/searchable'
10
+
11
+ config = YAML.load_file File.expand_path(File.dirname(__FILE__) + '/config.yml')
12
+ ActiveRecord::Base.establish_connection config.merge(:adapter => :postgresql)
13
+
14
+ class ARStandIn < ActiveRecord::Base;
15
+ self.abstract_class = true
16
+ extend Textacular
17
+ end
18
+
19
+ class NotThere < ARStandIn; end
20
+
21
+ class TextacularWebComic < ARStandIn;
22
+ has_many :characters, :foreign_key => :web_comic_id
23
+ self.table_name = :web_comics
24
+ end
25
+
26
+
27
+ class WebComic < ActiveRecord::Base
28
+ # string :name
29
+ # string :author
30
+ # integer :id
31
+
32
+ has_many :characters
33
+ end
34
+
35
+ class WebComicWithSearchable < WebComic
36
+ extend Searchable
37
+ end
38
+
39
+ class WebComicWithSearchableName < WebComic
40
+ extend Searchable(:name)
41
+ end
42
+
43
+ class WebComicWithSearchableNameAndAuthor < WebComic
44
+ extend Searchable(:name, :author)
45
+ end
46
+
47
+
48
+ class Character < ActiveRecord::Base
49
+ # string :name
50
+ # string :description
51
+ # integer :web_comic_id
52
+
53
+ belongs_to :web_comic
54
+ end
55
+
56
+
57
+ class Game < ActiveRecord::Base
58
+ # string :system
59
+ # string :title
60
+ # text :description
61
+ end
62
+
63
+ class GameExtendedWithTextacular < Game
64
+ extend Textacular
65
+ end
66
+
67
+ class GameExtendedWithTextacularAndCustomLanguage < GameExtendedWithTextacular
68
+ def searchable_language
69
+ 'spanish'
70
+ end
71
+ end
72
+
73
+
74
+ class GameFail < Game; end
75
+
76
+ class GameFailExtendedWithTextacular < GameFail
77
+ extend Textacular
78
+ end
@@ -0,0 +1,92 @@
1
+ require 'spec_helper'
2
+ require 'textacular/searchable'
3
+
4
+ class SearchableTest < Test::Unit::TestCase
5
+ context "when extending an ActiveRecord::Base subclass" do
6
+ context "with no parameters" do
7
+ setup do
8
+ @qcont = WebComicWithSearchable.create :name => "Questionable Content", :author => "Jeph Jaques"
9
+ @jhony = WebComicWithSearchable.create :name => "Johnny Wander", :author => "Ananth & Yuko"
10
+ @ddeeg = WebComicWithSearchable.create :name => "Dominic Deegan", :author => "Mookie"
11
+ @penny = WebComicWithSearchable.create :name => "Penny Arcade", :author => "Tycho & Gabe"
12
+ end
13
+
14
+ teardown do
15
+ WebComicWithSearchable.delete_all
16
+ end
17
+
18
+ should "search across all columns" do
19
+ assert_equal [@penny], WebComicWithSearchable.advanced_search("Penny")
20
+ assert_equal [@ddeeg], WebComicWithSearchable.advanced_search("Dominic")
21
+ end
22
+ end
23
+
24
+ context "with one column as parameter" do
25
+ setup do
26
+ @qcont = WebComicWithSearchableName.create :name => "Questionable Content", :author => "Jeph Jaques"
27
+ @jhony = WebComicWithSearchableName.create :name => "Johnny Wander", :author => "Ananth & Yuko"
28
+ @ddeeg = WebComicWithSearchableName.create :name => "Dominic Deegan", :author => "Mookie"
29
+ @penny = WebComicWithSearchableName.create :name => "Penny Arcade", :author => "Tycho & Gabe"
30
+ end
31
+
32
+ teardown do
33
+ WebComicWithSearchableName.delete_all
34
+ end
35
+
36
+ should "only search across the given column" do
37
+ assert_equal [@penny], WebComicWithSearchableName.advanced_search("Penny")
38
+ assert_empty WebComicWithSearchableName.advanced_search("Tycho")
39
+ end
40
+
41
+ ["hello \\", "tebow!" , "food &"].each do |search_term|
42
+ should "be fine with searching for crazy character #{search_term} with plain search" do
43
+ # Uses plainto_tsquery
44
+ assert_equal [], WebComicWithSearchableName.basic_search(search_term)
45
+ end
46
+
47
+ should "be not fine with searching for crazy character #{search_term} with advanced search" do
48
+ # Uses to_tsquery
49
+ assert_raise(ActiveRecord::StatementInvalid) do
50
+ WebComicWithSearchableName.advanced_search(search_term).all
51
+ end
52
+ end
53
+ end
54
+
55
+ should "fuzzy search stuff" do
56
+ assert_equal [@qcont], WebComicWithSearchableName.fuzzy_search('Questio')
57
+ end
58
+
59
+ should "define :searchable_columns as private" do
60
+ assert_raise(NoMethodError) { WebComicWithSearchableName.searchable_columns }
61
+ begin
62
+ WebComicWithSearchableName.searchable_columns
63
+ rescue NoMethodError => error
64
+ assert_match error.message, /private method/
65
+ end
66
+ end
67
+
68
+ should "define #indexable_columns which returns a write-proof Enumerable" do
69
+ assert_equal(Enumerator, WebComicWithSearchableName.indexable_columns.class)
70
+ assert_raise(NoMethodError) { WebComicWithSearchableName.indexable_columns[0] = 'foo' }
71
+ end
72
+ end
73
+
74
+ context "with two columns as parameters" do
75
+ setup do
76
+ @qcont = WebComicWithSearchableNameAndAuthor.create :name => "Questionable Content", :author => "Jeph Jaques"
77
+ @jhony = WebComicWithSearchableNameAndAuthor.create :name => "Johnny Wander", :author => "Ananth & Yuko"
78
+ @ddeeg = WebComicWithSearchableNameAndAuthor.create :name => "Dominic Deegan", :author => "Mookie"
79
+ @penny = WebComicWithSearchableNameAndAuthor.create :name => "Penny Arcade", :author => "Tycho & Gabe"
80
+ end
81
+
82
+ teardown do
83
+ WebComicWithSearchableNameAndAuthor.delete_all
84
+ end
85
+
86
+ should "only search across the given column" do
87
+ assert_equal [@penny], WebComicWithSearchableNameAndAuthor.advanced_search("Penny")
88
+ assert_equal [@penny], WebComicWithSearchableNameAndAuthor.advanced_search("Tycho")
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,216 @@
1
+ # coding: utf-8
2
+ require 'spec_helper'
3
+
4
+ class TextacularTest < Test::Unit::TestCase
5
+ context "after extending ActiveRecord::Base" do
6
+ should "not break #respond_to?" do
7
+ assert_nothing_raised do
8
+ ARStandIn.respond_to? :abstract_class?
9
+ end
10
+ end
11
+
12
+ should "not break #respond_to? for table-less classes" do
13
+ assert !NotThere.table_exists?
14
+ assert_nothing_raised do
15
+ NotThere.respond_to? :system
16
+ end
17
+ end
18
+
19
+ should "not break #method_missing" do
20
+ assert_raise(NoMethodError) { ARStandIn.random }
21
+ begin
22
+ ARStandIn.random
23
+ rescue NoMethodError => error
24
+ assert_match error.message, /undefined method `random'/
25
+ end
26
+ end
27
+
28
+ should "not break #method_missing for table-less classes" do
29
+ assert !NotThere.table_exists?
30
+ assert_raise(NoMethodError) { NotThere.random }
31
+ begin
32
+ NotThere.random
33
+ rescue NoMethodError => error
34
+ assert_match error.message, /undefined method `random'/
35
+ end
36
+ end
37
+
38
+ context "when finding models based on searching a related model" do
39
+ setup do
40
+ @qc = TextacularWebComic.create :name => "Questionable Content", :author => "Jeph Jaques"
41
+ @jw = TextacularWebComic.create :name => "Johnny Wander", :author => "Ananth & Yuko"
42
+ @pa = TextacularWebComic.create :name => "Penny Arcade", :author => "Tycho & Gabe"
43
+
44
+ @gabe = @pa.characters.create :name => 'Gabe', :description => 'the simple one'
45
+ @tycho = @pa.characters.create :name => 'Tycho', :description => 'the wordy one'
46
+ @div = @pa.characters.create :name => 'Div', :description => 'a crude divx player with anger management issues'
47
+
48
+ @martin = @qc.characters.create :name => 'Martin', :description => 'the insecure protagonist'
49
+ @faye = @qc.characters.create :name => 'Faye', :description => 'a sarcastic barrista with anger management issues'
50
+ @pintsize = @qc.characters.create :name => 'Pintsize', :description => 'a crude AnthroPC'
51
+
52
+ @ananth = @jw.characters.create :name => 'Ananth', :description => 'Stubble! What is under that hat?!?'
53
+ @yuko = @jw.characters.create :name => 'Yuko', :description => 'So... small. Carl Sagan haircut.'
54
+ @john = @jw.characters.create :name => 'John', :description => 'Tall. Anger issues?'
55
+ @cricket = @jw.characters.create :name => 'Cricket', :description => 'Chirrup!'
56
+ end
57
+
58
+ teardown do
59
+ TextacularWebComic.delete_all
60
+ Character.delete_all
61
+ end
62
+
63
+ should "look in the related model with nested searching syntax" do
64
+ assert_equal [@jw], TextacularWebComic.joins(:characters).advanced_search(:characters => {:description => 'tall'})
65
+ assert_equal [@pa, @jw, @qc].sort, TextacularWebComic.joins(:characters).advanced_search(:characters => {:description => 'anger'}).sort
66
+ assert_equal [@pa, @qc].sort, TextacularWebComic.joins(:characters).advanced_search(:characters => {:description => 'crude'}).sort
67
+ end
68
+ end
69
+ end
70
+
71
+ context "after extending an ActiveRecord::Base subclass" do
72
+ setup do
73
+ @zelda = GameExtendedWithTextacular.create :system => "NES", :title => "Legend of Zelda", :description => "A Link to the Past."
74
+ @mario = GameExtendedWithTextacular.create :system => "NES", :title => "Super Mario Bros.", :description => "The original platformer."
75
+ @sonic = GameExtendedWithTextacular.create :system => "Genesis", :title => "Sonic the Hedgehog", :description => "Spiky."
76
+ @dkong = GameExtendedWithTextacular.create :system => "SNES", :title => "Diddy's Kong Quest", :description => "Donkey Kong Country 2"
77
+ @megam = GameExtendedWithTextacular.create :system => nil, :title => "Mega Man", :description => "Beware Dr. Brain"
78
+ @sfnes = GameExtendedWithTextacular.create :system => "SNES", :title => "Street Fighter 2", :description => "Yoga Flame!"
79
+ @sfgen = GameExtendedWithTextacular.create :system => "Genesis", :title => "Street Fighter 2", :description => "Yoga Flame!"
80
+ @takun = GameExtendedWithTextacular.create :system => "Saturn", :title => "Magical Tarurūto-kun", :description => "カッコイイ!"
81
+ end
82
+
83
+ teardown do
84
+ GameExtendedWithTextacular.delete_all
85
+ end
86
+
87
+ should "not break respond_to? when connection is unavailable" do
88
+ GameFailExtendedWithTextacular.establish_connection({:adapter => :postgresql, :database =>'unavailable', :username=>'bad', :pool=>5, :timeout=>5000}) rescue nil
89
+
90
+ assert_nothing_raised do
91
+ GameFailExtendedWithTextacular.respond_to?(:advanced_search)
92
+ end
93
+ end
94
+
95
+ should "define a #search method" do
96
+ assert GameExtendedWithTextacular.respond_to?(:search)
97
+ end
98
+
99
+ context "when searching with a String argument" do
100
+ should "search across all :string columns if no indexes have been specified" do
101
+ assert_equal [@mario], GameExtendedWithTextacular.advanced_search("Mario")
102
+ assert_equal Set.new([@mario, @zelda]), GameExtendedWithTextacular.advanced_search("NES").to_set
103
+ end
104
+
105
+ should "work if the query contains an apostrophe" do
106
+ assert_equal [@dkong], GameExtendedWithTextacular.advanced_search("Diddy's")
107
+ end
108
+
109
+ should "work if the query contains whitespace" do
110
+ assert_equal [@megam], GameExtendedWithTextacular.advanced_search("Mega Man")
111
+ end
112
+
113
+ should "work if the query contains an accent" do
114
+ assert_equal [@takun], GameExtendedWithTextacular.advanced_search("Tarurūto-kun")
115
+ end
116
+
117
+ should "search across records with NULL values" do
118
+ assert_equal [@megam], GameExtendedWithTextacular.advanced_search("Mega")
119
+ end
120
+
121
+ should "scope consecutively" do
122
+ assert_equal [@sfgen], GameExtendedWithTextacular.advanced_search("Genesis").advanced_search("Street Fighter")
123
+ end
124
+ end
125
+
126
+ context "when searching with a Hash argument" do
127
+ should "search across the given columns" do
128
+ assert_empty GameExtendedWithTextacular.advanced_search(:title => "NES")
129
+ assert_empty GameExtendedWithTextacular.advanced_search(:system => "Mario")
130
+ assert_empty GameExtendedWithTextacular.advanced_search(:system => "NES", :title => "Sonic")
131
+
132
+ assert_equal [@mario], GameExtendedWithTextacular.advanced_search(:title => "Mario")
133
+
134
+ assert_equal 2, GameExtendedWithTextacular.advanced_search(:system => "NES").count
135
+
136
+ assert_equal [@zelda], GameExtendedWithTextacular.advanced_search(:system => "NES", :title => "Zelda")
137
+ assert_equal [@megam], GameExtendedWithTextacular.advanced_search(:title => "Mega")
138
+ end
139
+
140
+ should "scope consecutively" do
141
+ assert_equal [@sfgen], GameExtendedWithTextacular.advanced_search(:system => "Genesis").advanced_search(:title => "Street Fighter")
142
+ end
143
+
144
+ should "cast non-:string columns as text" do
145
+ assert_equal [@mario], GameExtendedWithTextacular.advanced_search(:id => @mario.id)
146
+ end
147
+ end
148
+
149
+ context "when using dynamic search methods" do
150
+ should "generate methods for each :string column" do
151
+ assert_equal [@mario], GameExtendedWithTextacular.advanced_search_by_title("Mario")
152
+ assert_equal [@takun], GameExtendedWithTextacular.advanced_search_by_system("Saturn")
153
+ end
154
+
155
+ should "generate methods for each :text column" do
156
+ assert_equal [@mario], GameExtendedWithTextacular.advanced_search_by_description("platform")
157
+ end
158
+
159
+ should "generate methods for any combination of :string and :text columns" do
160
+ assert_equal [@mario], GameExtendedWithTextacular.advanced_search_by_title_and_system("Mario", "NES")
161
+ assert_equal [@sonic], GameExtendedWithTextacular.advanced_search_by_system_and_title("Genesis", "Sonic")
162
+ assert_equal [@mario], GameExtendedWithTextacular.advanced_search_by_title_and_title("Mario", "Mario")
163
+ assert_equal [@megam], GameExtendedWithTextacular.advanced_search_by_title_and_description("Man", "Brain")
164
+ end
165
+
166
+ should "generate methods for inclusive searches" do
167
+ assert_equal Set.new([@megam, @takun]), GameExtendedWithTextacular.advanced_search_by_system_or_title("Saturn", "Mega Man").to_set
168
+ end
169
+
170
+ should "scope consecutively" do
171
+ assert_equal [@sfgen], GameExtendedWithTextacular.advanced_search_by_system("Genesis").advanced_search_by_title("Street Fighter")
172
+ end
173
+
174
+ should "generate methods for non-:string columns" do
175
+ assert_equal [@mario], GameExtendedWithTextacular.advanced_search_by_id(@mario.id)
176
+ end
177
+
178
+ should "work with #respond_to?" do
179
+ assert GameExtendedWithTextacular.respond_to?(:advanced_search_by_system)
180
+ assert GameExtendedWithTextacular.respond_to?(:advanced_search_by_title)
181
+ assert GameExtendedWithTextacular.respond_to?(:advanced_search_by_system_and_title)
182
+ assert GameExtendedWithTextacular.respond_to?(:advanced_search_by_system_or_title)
183
+ assert GameExtendedWithTextacular.respond_to?(:advanced_search_by_title_and_title_and_title)
184
+ assert GameExtendedWithTextacular.respond_to?(:advanced_search_by_id)
185
+
186
+ assert !GameExtendedWithTextacular.respond_to?(:advanced_search_by_title_and_title_or_title)
187
+ end
188
+
189
+ should "allow for 2 arguments to #respond_to?" do
190
+ assert GameExtendedWithTextacular.respond_to?(:normalize, true)
191
+ end
192
+ end
193
+
194
+ context "when searching after selecting columns to return" do
195
+ should "not fetch extra columns" do
196
+ assert_raise(ActiveModel::MissingAttributeError) do
197
+ GameExtendedWithTextacular.select(:title).advanced_search("Mario").first.system
198
+ end
199
+ end
200
+ end
201
+
202
+ context "when setting a custom search language" do
203
+ setup do
204
+ GameExtendedWithTextacularAndCustomLanguage.create :system => "PS3", :title => "Harry Potter & the Deathly Hallows"
205
+ end
206
+
207
+ teardown do
208
+ GameExtendedWithTextacularAndCustomLanguage.delete_all
209
+ end
210
+
211
+ should "still find results" do
212
+ assert_not_empty GameExtendedWithTextacularAndCustomLanguage.advanced_search_by_title("harry")
213
+ end
214
+ end
215
+ end
216
+ end
metadata ADDED
@@ -0,0 +1,178 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: textacular
3
+ version: !ruby/object:Gem::Version
4
+ version: 3.0.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Ben Hamill
9
+ - ecin
10
+ - Aaron Patterson
11
+ autorequire:
12
+ bindir: bin
13
+ cert_chain: []
14
+ date: 2013-02-27 00:00:00.000000000 Z
15
+ dependencies:
16
+ - !ruby/object:Gem::Dependency
17
+ name: pg
18
+ requirement: !ruby/object:Gem::Requirement
19
+ none: false
20
+ requirements:
21
+ - - ~>
22
+ - !ruby/object:Gem::Version
23
+ version: 0.11.0
24
+ type: :development
25
+ prerelease: false
26
+ version_requirements: !ruby/object:Gem::Requirement
27
+ none: false
28
+ requirements:
29
+ - - ~>
30
+ - !ruby/object:Gem::Version
31
+ version: 0.11.0
32
+ - !ruby/object:Gem::Dependency
33
+ name: shoulda
34
+ requirement: !ruby/object:Gem::Requirement
35
+ none: false
36
+ requirements:
37
+ - - ~>
38
+ - !ruby/object:Gem::Version
39
+ version: 2.11.3
40
+ type: :development
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ none: false
44
+ requirements:
45
+ - - ~>
46
+ - !ruby/object:Gem::Version
47
+ version: 2.11.3
48
+ - !ruby/object:Gem::Dependency
49
+ name: rake
50
+ requirement: !ruby/object:Gem::Requirement
51
+ none: false
52
+ requirements:
53
+ - - ~>
54
+ - !ruby/object:Gem::Version
55
+ version: 0.9.0
56
+ type: :development
57
+ prerelease: false
58
+ version_requirements: !ruby/object:Gem::Requirement
59
+ none: false
60
+ requirements:
61
+ - - ~>
62
+ - !ruby/object:Gem::Version
63
+ version: 0.9.0
64
+ - !ruby/object:Gem::Dependency
65
+ name: pry
66
+ requirement: !ruby/object:Gem::Requirement
67
+ none: false
68
+ requirements:
69
+ - - ! '>='
70
+ - !ruby/object:Gem::Version
71
+ version: '0'
72
+ type: :development
73
+ prerelease: false
74
+ version_requirements: !ruby/object:Gem::Requirement
75
+ none: false
76
+ requirements:
77
+ - - ! '>='
78
+ - !ruby/object:Gem::Version
79
+ version: '0'
80
+ - !ruby/object:Gem::Dependency
81
+ name: pry-doc
82
+ requirement: !ruby/object:Gem::Requirement
83
+ none: false
84
+ requirements:
85
+ - - ! '>='
86
+ - !ruby/object:Gem::Version
87
+ version: '0'
88
+ type: :development
89
+ prerelease: false
90
+ version_requirements: !ruby/object:Gem::Requirement
91
+ none: false
92
+ requirements:
93
+ - - ! '>='
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ - !ruby/object:Gem::Dependency
97
+ name: activerecord
98
+ requirement: !ruby/object:Gem::Requirement
99
+ none: false
100
+ requirements:
101
+ - - ! '>='
102
+ - !ruby/object:Gem::Version
103
+ version: '3.0'
104
+ - - <
105
+ - !ruby/object:Gem::Version
106
+ version: '4.1'
107
+ type: :runtime
108
+ prerelease: false
109
+ version_requirements: !ruby/object:Gem::Requirement
110
+ none: false
111
+ requirements:
112
+ - - ! '>='
113
+ - !ruby/object:Gem::Version
114
+ version: '3.0'
115
+ - - <
116
+ - !ruby/object:Gem::Version
117
+ version: '4.1'
118
+ description: ! "Textacular exposes full text search capabilities from PostgreSQL,
119
+ extending\n ActiveRecord with scopes making search easy and fun!"
120
+ email:
121
+ - git-commits@benhamill.com
122
+ - ecin@copypastel.com
123
+ executables: []
124
+ extensions: []
125
+ extra_rdoc_files: []
126
+ files:
127
+ - CHANGELOG.md
128
+ - Gemfile
129
+ - README.md
130
+ - Rakefile
131
+ - lib/textacular.rb
132
+ - lib/textacular/full_text_indexer.rb
133
+ - lib/textacular/postgres_module_installer.rb
134
+ - lib/textacular/rails.rb
135
+ - lib/textacular/searchable.rb
136
+ - lib/textacular/tasks.rb
137
+ - lib/textacular/version.rb
138
+ - spec/config.yml.example
139
+ - spec/fixtures/character.rb
140
+ - spec/fixtures/game.rb
141
+ - spec/fixtures/webcomic.rb
142
+ - spec/spec_helper.rb
143
+ - spec/textacular/searchable_spec.rb
144
+ - spec/textacular_spec.rb
145
+ homepage: http://textacular.github.com/textacular
146
+ licenses:
147
+ - MIT
148
+ post_install_message:
149
+ rdoc_options: []
150
+ require_paths:
151
+ - lib
152
+ required_ruby_version: !ruby/object:Gem::Requirement
153
+ none: false
154
+ requirements:
155
+ - - ! '>='
156
+ - !ruby/object:Gem::Version
157
+ version: '0'
158
+ required_rubygems_version: !ruby/object:Gem::Requirement
159
+ none: false
160
+ requirements:
161
+ - - ! '>='
162
+ - !ruby/object:Gem::Version
163
+ version: '0'
164
+ requirements: []
165
+ rubyforge_project:
166
+ rubygems_version: 1.8.23
167
+ signing_key:
168
+ specification_version: 3
169
+ summary: Textacular exposes full text search capabilities from PostgreSQL
170
+ test_files:
171
+ - spec/config.yml.example
172
+ - spec/fixtures/character.rb
173
+ - spec/fixtures/game.rb
174
+ - spec/fixtures/webcomic.rb
175
+ - spec/spec_helper.rb
176
+ - spec/textacular/searchable_spec.rb
177
+ - spec/textacular_spec.rb
178
+ has_rdoc: