texticle 2.0.3 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,3 +1,24 @@
1
+ == 2.1.0
2
+
3
+ * 1 DEPRECATION
4
+
5
+ * `search` aliases new `advanced_search` method (same functionality as before), but will
6
+ alias `basic_search` in 3.0! Should print warnings.
7
+
8
+ * 3 new features
9
+
10
+ * Generate full text search indexes from a rake task (sort of like in 1.x). Supply a specific
11
+ model name.
12
+ * New search methods: `basic_search`, `advanced_search` and `fuzzy_search`. Basic allows special
13
+ characters like &, and % in search terms. Fuzzy is based on Postgres's trigram matching extension
14
+ pg_trgm. Advanced is the same functionality from `search` previously.
15
+ * Rake task that installs pg_trgm now works on Postgres 9.1 and up.
16
+
17
+ * 2 dev improvements
18
+
19
+ * Test database configuration not automatically generated from a rake task and ignored by git.
20
+ * New interactive developer console (powered by pry).
21
+
1
22
  == 2.0.3
2
23
 
3
24
  * 1 new feature
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
@@ -38,6 +38,20 @@ Your models now have access to the search method:
38
38
  Game.search_by_title_and_system('Final Fantasy', 'PS2')
39
39
  Game.search_by_title_or_system('Final Fantasy, 'PS3')
40
40
 
41
+ You can use '|' and '&' for logical conditions.
42
+
43
+ Game.search_by_title_or_system('Final Fantasy', 'PS3|Xbox')
44
+
45
+ === Setting Language
46
+
47
+ To set proper searching dictionary just override class method on your model:
48
+
49
+ def self.searchable_language
50
+ 'russian'
51
+ end
52
+
53
+ And all your queries would go right! And don`t forget to change the migration for indexes, like shown below.
54
+
41
55
  === Creating Indexes for Super Speed
42
56
  You can have Postgresql use an index for the full-text search. To declare a full-text index, in a
43
57
  migration add code like the following:
@@ -50,6 +64,9 @@ In the above example, the table email_logs has two text columns that we search a
50
64
  You will need to add an index for every text/string column you query against, or else Postgresql will revert to a
51
65
  full table scan instead of using the indexes.
52
66
 
67
+ If you create these indexes, you should also switch to sql for your schema_format in `config/application.rb`:
68
+
69
+ config.active_record.schema_format = :sql
53
70
 
54
71
  == REQUIREMENTS:
55
72
 
data/Rakefile CHANGED
@@ -1,6 +1,7 @@
1
1
  require 'rubygems'
2
2
 
3
3
  require 'rake'
4
+ require 'yaml'
4
5
  require 'pg'
5
6
  require 'active_record'
6
7
  require 'benchmark'
@@ -8,29 +9,115 @@ require 'benchmark'
8
9
  $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/spec')
9
10
 
10
11
  task :default do
11
- config = File.open(File.expand_path(File.dirname(__FILE__) + '/spec/config.yml')).read
12
- if config.match /<username>/
13
- print "Would you like to create and configure the test database? y/n "
14
- continue = STDIN.getc
15
- exit 0 unless continue == "Y" || continue == "y"
16
- sh "createdb texticle"
17
- File.open(File.expand_path(File.dirname(__FILE__) + '/spec/config.yml'), "w") do |writable_config|
18
- writable_config << config.sub(/<username>/, `whoami`.chomp)
19
- end
20
- Rake::Task["db:migrate"].invoke
21
- end
12
+ Rake::Task["db:setup"].invoke
22
13
  Rake::Task["test"].invoke
23
14
  end
24
15
 
16
+ desc "Fire up an interactive terminal to play with"
17
+ task :console do
18
+ require 'pry'
19
+ require File.expand_path(File.dirname(__FILE__) + '/lib/texticle')
20
+
21
+ config = YAML.load_file File.expand_path(File.dirname(__FILE__) + '/spec/config.yml')
22
+ ActiveRecord::Base.establish_connection config.merge(:adapter => :postgresql)
23
+
24
+ class Character < ActiveRecord::Base
25
+ belongs_to :web_comic
26
+ end
27
+
28
+ class WebComic < ActiveRecord::Base
29
+ has_many :characters
30
+ end
31
+
32
+ class Game < ActiveRecord::Base
33
+ end
34
+
35
+ # add ability to reload console
36
+ def reload
37
+ reload_msg = '# Reloading the console...'
38
+ puts CodeRay.scan(reload_msg, :ruby).term
39
+ Pry.save_history
40
+ exec('rake console')
41
+ end
42
+
43
+ # start the console! :-)
44
+ welcome = <<-EOS
45
+ Welcome to the Texticle devloper console. You have some classes you can play with:
46
+
47
+ class Character < ActiveRecord::Base
48
+ # string :name
49
+ # string :description
50
+ # integer :web_comic_id
51
+
52
+ belongs_to :web_comic
53
+ end
54
+
55
+ class WebComic < ActiveRecord::Base
56
+ # string :name
57
+ # string :author
58
+ # integer :id
59
+
60
+ has_many :characters
61
+ end
62
+
63
+ class Game < ActiveRecord::Base
64
+ # string :system
65
+ # string :title
66
+ # text :description
67
+ end
68
+ EOS
69
+
70
+ puts CodeRay.scan(welcome, :ruby).term
71
+ Pry.start
72
+ end
73
+
25
74
  task :test do
26
75
  require 'texticle_spec'
27
76
  require 'texticle/searchable_spec'
77
+ require 'texticle/full_text_indexer_spec'
28
78
  end
29
79
 
30
80
  namespace :db do
81
+ desc 'Create and configure the test database'
82
+ task :setup do
83
+ spec_directory = "#{File.expand_path(File.dirname(__FILE__))}/spec"
84
+
85
+ STDOUT.puts "Detecting database configuration..."
86
+
87
+ if File.exists?("#{spec_directory}/config.yml")
88
+ STDOUT.puts "Configuration detected. Skipping confguration."
89
+ else
90
+ STDOUT.puts "Would you like to create and configure the test database? y/N"
91
+ continue = STDIN.gets.chomp
92
+
93
+ unless continue =~ /^[y]$/i
94
+ STDOUT.puts "Done."
95
+ exit 0
96
+ end
97
+
98
+ STDOUT.puts "Creating database..."
99
+ `createdb texticle`
100
+
101
+ STDOUT.puts "Writing configuration file..."
102
+
103
+ config_example = File.read("#{spec_directory}/config.yml.example")
104
+
105
+ File.open("#{spec_directory}/config.yml", "w") do |config|
106
+ config << config_example.sub(/<username>/, `whoami`.chomp)
107
+ end
108
+
109
+ STDOUT.puts "Running migrations..."
110
+ Rake::Task["db:migrate"].invoke
111
+
112
+ STDOUT.puts 'Done.'
113
+ end
114
+ end
115
+
31
116
  desc 'Run migrations for test database'
32
117
  task :migrate do
33
- require 'spec_helper'
118
+ config = YAML.load_file File.expand_path(File.dirname(__FILE__) + '/spec/config.yml')
119
+ ActiveRecord::Base.establish_connection config.merge(:adapter => :postgresql)
120
+
34
121
  ActiveRecord::Migration.instance_eval do
35
122
  create_table :games do |table|
36
123
  table.string :system
@@ -38,11 +125,13 @@ namespace :db do
38
125
  table.text :description
39
126
  end
40
127
  create_table :web_comics do |table|
128
+
41
129
  table.string :name
42
130
  table.string :author
43
131
  table.text :review
44
132
  table.integer :id
45
133
  end
134
+
46
135
  create_table :characters do |table|
47
136
  table.string :name
48
137
  table.string :description
@@ -50,9 +139,12 @@ namespace :db do
50
139
  end
51
140
  end
52
141
  end
142
+
53
143
  desc 'Drop tables from test database'
54
144
  task :drop do
55
- require 'spec_helper'
145
+ config = YAML.load_file File.expand_path(File.dirname(__FILE__) + '/spec/config.yml')
146
+ ActiveRecord::Base.establish_connection config.merge(:adapter => :postgresql)
147
+
56
148
  ActiveRecord::Migration.instance_eval do
57
149
  drop_table :games
58
150
  drop_table :web_comics
@@ -1,24 +1,36 @@
1
1
  require 'active_record'
2
2
 
3
+ require 'texticle/version'
4
+
3
5
  module Texticle
4
- def search(query = "", exclusive = true)
5
- @similarities = []
6
- @conditions = []
6
+ def self.searchable_language
7
+ 'english'
8
+ end
7
9
 
8
- unless query.is_a?(Hash)
9
- exclusive = false
10
- query = searchable_columns.inject({}) do |terms, column|
11
- terms.merge column => query.to_s
12
- end
13
- end
10
+ def search(query = "", exclusive = true)
11
+ warn "[DEPRECATION] `search` is deprecated. Please use `advanced_search` instead. At the next major release `search` will become an alias for `basic_search`."
12
+ advanced_search(query, exclusive)
13
+ end
14
14
 
15
- parse_query_hash(query)
15
+ def basic_search(query = "", exclusive = true)
16
+ exclusive, query = munge_exclusive_and_query(exclusive, query)
17
+ parsed_query_hash = parse_query_hash(query)
18
+ similarities, conditions = basic_similarities_and_conditions(parsed_query_hash)
19
+ assemble_query(similarities, conditions, exclusive)
20
+ end
16
21
 
17
- rank = connection.quote_column_name('rank' + rand.to_s)
22
+ def advanced_search(query = "", exclusive = true)
23
+ exclusive, query = munge_exclusive_and_query(exclusive, query)
24
+ parsed_query_hash = parse_query_hash(query)
25
+ similarities, conditions = advanced_similarities_and_conditions(parsed_query_hash)
26
+ assemble_query(similarities, conditions, exclusive)
27
+ end
18
28
 
19
- select("#{quoted_table_name + '.*,' if scoped.select_values.empty?} #{@similarities.join(" + ")} AS #{rank}").
20
- where(@conditions.join(exclusive ? " AND " : " OR ")).
21
- order("#{rank} DESC")
29
+ def fuzzy_search(query = '', exclusive = true)
30
+ exclusive, query = munge_exclusive_and_query(exclusive, query)
31
+ parsed_query_hash = parse_query_hash(query)
32
+ similarities, conditions = fuzzy_similarities_and_conditions(parsed_query_hash)
33
+ assemble_query(similarities, conditions, exclusive)
22
34
  end
23
35
 
24
36
  def method_missing(method, *search_terms)
@@ -31,7 +43,7 @@ module Texticle
31
43
  query = columns.inject({}) do |query, column|
32
44
  query.merge column => args.shift
33
45
  end
34
- search(query, exclusive)
46
+ self.send(Helper.search_type(method), query, exclusive)
35
47
  end
36
48
  __send__(method, *search_terms, exclusive)
37
49
  else
@@ -50,20 +62,93 @@ module Texticle
50
62
 
51
63
  private
52
64
 
65
+ def munge_exclusive_and_query(exclusive, query)
66
+ unless query.is_a?(Hash)
67
+ exclusive = false
68
+ query = searchable_columns.inject({}) do |terms, column|
69
+ terms.merge column => query.to_s
70
+ end
71
+ end
72
+
73
+ [exclusive, query]
74
+ end
75
+
53
76
  def parse_query_hash(query, table_name = quoted_table_name)
54
- language = connection.quote(searchable_language)
55
77
  table_name = connection.quote_table_name(table_name)
56
78
 
79
+ results = []
80
+
57
81
  query.each do |column_or_table, search_term|
58
82
  if search_term.is_a?(Hash)
59
- parse_query_hash(search_term, column_or_table)
83
+ results += parse_query_hash(search_term, column_or_table)
60
84
  else
61
85
  column = connection.quote_column_name(column_or_table)
62
86
  search_term = connection.quote normalize(Helper.normalize(search_term))
63
- @similarities << "ts_rank(to_tsvector(#{language}, #{table_name}.#{column}::text), to_tsquery(#{language}, #{search_term}::text))"
64
- @conditions << "to_tsvector(#{language}, #{table_name}.#{column}::text) @@ to_tsquery(#{language}, #{search_term}::text)"
87
+
88
+ results << [table_name, column, search_term]
65
89
  end
66
90
  end
91
+
92
+ results
93
+ end
94
+
95
+ def basic_similarities_and_conditions(parsed_query_hash)
96
+ parsed_query_hash.inject([[], []]) do |(similarities, conditions), query_args|
97
+ similarities << basic_similarity_string(*query_args)
98
+ conditions << basic_condition_string(*query_args)
99
+
100
+ [similarities, conditions]
101
+ end
102
+ end
103
+
104
+ def basic_similarity_string(table_name, column, search_term)
105
+ "ts_rank(to_tsvector(#{quoted_language}, #{table_name}.#{column}::text), plainto_tsquery(#{quoted_language}, #{search_term}::text))"
106
+ end
107
+
108
+ def basic_condition_string(table_name, column, search_term)
109
+ "to_tsvector(#{quoted_language}, #{table_name}.#{column}::text) @@ plainto_tsquery(#{quoted_language}, #{search_term}::text)"
110
+ end
111
+
112
+ def advanced_similarities_and_conditions(parsed_query_hash)
113
+ parsed_query_hash.inject([[], []]) do |(similarities, conditions), query_args|
114
+ similarities << advanced_similarity_string(*query_args)
115
+ conditions << advanced_condition_string(*query_args)
116
+
117
+ [similarities, conditions]
118
+ end
119
+ end
120
+
121
+ def advanced_similarity_string(table_name, column, search_term)
122
+ "ts_rank(to_tsvector(#{quoted_language}, #{table_name}.#{column}::text), to_tsquery(#{quoted_language}, #{search_term}::text))"
123
+ end
124
+
125
+ def advanced_condition_string(table_name, column, search_term)
126
+ "to_tsvector(#{quoted_language}, #{table_name}.#{column}::text) @@ to_tsquery(#{quoted_language}, #{search_term}::text)"
127
+ end
128
+
129
+ def fuzzy_similarities_and_conditions(parsed_query_hash)
130
+ parsed_query_hash.inject([[], []]) do |(similarities, conditions), query_args|
131
+ similarities << fuzzy_similarity_string(*query_args)
132
+ conditions << fuzzy_condition_string(*query_args)
133
+
134
+ [similarities, conditions]
135
+ end
136
+ end
137
+
138
+ def fuzzy_similarity_string(table_name, column, search_term)
139
+ "similarity(#{table_name}.#{column}, #{search_term})"
140
+ end
141
+
142
+ def fuzzy_condition_string(table_name, column, search_term)
143
+ "(#{table_name}.#{column} % #{search_term})"
144
+ end
145
+
146
+ def assemble_query(similarities, conditions, exclusive)
147
+ rank = connection.quote_column_name('rank' + rand.to_s)
148
+
149
+ select("#{quoted_table_name + '.*,' if scoped.select_values.empty?} #{similarities.join(" + ")} AS #{rank}").
150
+ where(conditions.join(exclusive ? " AND " : " OR ")).
151
+ order("#{rank} DESC")
67
152
  end
68
153
 
69
154
  def normalize(query)
@@ -74,8 +159,12 @@ module Texticle
74
159
  columns.select {|column| [:string, :text].include? column.type }.map(&:name)
75
160
  end
76
161
 
162
+ def quoted_language
163
+ @quoted_language ||= connection.quote(searchable_language)
164
+ end
165
+
77
166
  def searchable_language
78
- 'english'
167
+ Texticle.searchable_language
79
168
  end
80
169
 
81
170
  module Helper
@@ -84,8 +173,16 @@ module Texticle
84
173
  query.to_s.gsub(' ', '\\\\ ')
85
174
  end
86
175
 
176
+ def method_name_regex
177
+ /^(?<search_type>((basic|advanced|fuzzy)_)?search)_by_(?<columns>[_a-zA-Z]\w*)$/
178
+ end
179
+
180
+ def search_type(method)
181
+ method.to_s.match(method_name_regex)[:search_type]
182
+ end
183
+
87
184
  def exclusive_dynamic_search_columns(method)
88
- if match = method.to_s.match(/^search_by_(?<columns>[_a-zA-Z]\w*)$/)
185
+ if match = method.to_s.match(method_name_regex)
89
186
  match[:columns].split('_and_')
90
187
  else
91
188
  []
@@ -93,7 +190,7 @@ module Texticle
93
190
  end
94
191
 
95
192
  def inclusive_dynamic_search_columns(method)
96
- if match = method.to_s.match(/^search_by_(?<columns>[_a-zA-Z]\w*)$/)
193
+ if match = method.to_s.match(method_name_regex)
97
194
  match[:columns].split('_or_')
98
195
  else
99
196
  []
@@ -127,3 +224,5 @@ module Texticle
127
224
  end
128
225
  end
129
226
  end
227
+
228
+ require File.expand_path(File.dirname(__FILE__) + '/texticle/full_text_indexer')
@@ -0,0 +1,79 @@
1
+ class Texticle::FullTextIndexer
2
+ def generate_migration(model_name)
3
+ stream_output do |io|
4
+ io.puts(<<-MIGRATION)
5
+ class #{model_name}FullTextSearch < ActiveRecord::Migration
6
+ def self.up
7
+ execute(<<-SQL.strip)
8
+ #{up_migration(model_name)}
9
+ SQL
10
+ end
11
+
12
+ def self.down
13
+ execute(<<-SQL.strip)
14
+ #{down_migration(model_name)}
15
+ SQL
16
+ end
17
+ end
18
+ MIGRATION
19
+ end
20
+ end
21
+
22
+ def stream_output(now = Time.now.utc, &block)
23
+ if !@output_stream && defined?(Rails)
24
+ File.open(migration_file_name(now), 'w', &block)
25
+ else
26
+ @output_stream ||= $stdout
27
+
28
+ yield @output_stream
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def migration_file_name(now = Time.now.utc)
35
+ File.join(Rails.root, 'db', 'migrate',"#{now.strftime('%Y%m%d%H%M%S')}_full_text_search.rb")
36
+ end
37
+
38
+ def up_migration(model_name)
39
+ migration_with_type(model_name, :up)
40
+ end
41
+
42
+ def down_migration(model_name)
43
+ migration_with_type(model_name, :down)
44
+ end
45
+
46
+ def migration_with_type(model_name, type)
47
+ sql_lines = ''
48
+
49
+ model = Kernel.const_get(model_name)
50
+ model.indexable_columns.each do |column|
51
+ sql_lines << drop_index_sql_for(model, column)
52
+ sql_lines << create_index_sql_for(model, column) if type == :up
53
+ end
54
+
55
+ sql_lines.strip.gsub("\n","\n ")
56
+ end
57
+
58
+ def drop_index_sql_for(model, column)
59
+ "DROP index IF EXISTS #{index_name_for(model, column)};\n"
60
+ end
61
+
62
+ def create_index_sql_for(model, column)
63
+ # The spacing gets sort of wonky in here.
64
+
65
+ <<-SQL
66
+ CREATE index #{index_name_for(model, column)}
67
+ ON #{model.table_name}
68
+ USING gin(to_tsvector("#{dictionary}", "#{model.table_name}"."#{column}"::text));
69
+ SQL
70
+ end
71
+
72
+ def index_name_for(model, column)
73
+ "#{model.table_name}_#{column}_fts_idx"
74
+ end
75
+
76
+ def dictionary
77
+ Texticle.searchable_language
78
+ end
79
+ end
@@ -0,0 +1,57 @@
1
+ module Texticle
2
+ class PostgresModuleInstaller
3
+ def install_module(module_name)
4
+ major, minor, patch = postgres_version.split('.')
5
+
6
+ if major.to_i >= 9 && minor.to_i >= 1
7
+ install_postgres_91_module(module_name)
8
+ else
9
+ install_postgres_90_module(module_name)
10
+ end
11
+ end
12
+
13
+ def db_name
14
+ @db_name ||= ActiveRecord::Base.connection.current_database
15
+ end
16
+
17
+ private
18
+
19
+ def postgres_version
20
+ @postgres_version ||= ask_pg_config('version').match(/PostgreSQL ([0-9]+(\.[0-9]+)*)/)[1]
21
+ end
22
+
23
+ def postgres_share_dir
24
+ @share_dir ||= ask_pg_config('sharedir')
25
+ end
26
+
27
+ def ask_pg_config(argument)
28
+ result = `pg_config --#{argument}`.chomp
29
+
30
+ raise RuntimeError, "Cannot find Postgres's #{argument}." unless $?.success?
31
+
32
+ result
33
+ end
34
+
35
+ def install_postgres_90_module(module_name)
36
+ module_location = "#{postgres_share_dir}/contrib/#{module_name}.sql"
37
+
38
+ unless system("ls #{module_location}")
39
+ raise RuntimeError, "Cannot find the #{module_name} module. Was it compiled and installed?"
40
+ end
41
+
42
+ unless system("psql -d #{db_name} -f #{module_location}")
43
+ raise RuntimeError, "`psql -d #{db_name} -f #{module_location}` cannot complete successfully."
44
+ end
45
+ end
46
+
47
+ def install_postgres_91_module(module_name)
48
+ module_location = "#{postgres_share_dir}/extension/#{module_name}.control"
49
+
50
+ unless system("ls #{module_location}")
51
+ raise RuntimeError, "Cannot find the #{module_name} module. Was it compiled and installed?"
52
+ end
53
+
54
+ ActiveRecord::Base.connection.execute("CREATE EXTENSION #{module_name};")
55
+ end
56
+ end
57
+ end
@@ -10,6 +10,10 @@ def Searchable(*searchable_columns)
10
10
  end
11
11
 
12
12
  private :searchable_columns
13
+
14
+ def indexable_columns
15
+ searchable_columns.to_enum
16
+ end
13
17
  end
14
18
  end
15
19
 
@@ -0,0 +1,18 @@
1
+ require 'rake'
2
+ require 'texticle'
3
+
4
+ namespace :texticle do
5
+ desc 'Create full text search index migration, give the model for which you want to create the indexes'
6
+ task :create_index_migration, [:model_name] => :environment do |task, args|
7
+ raise 'A model name is required' unless args[:model_name]
8
+ Texticle::FullTextIndexer.new.generate_migration(args[:model_name])
9
+ end
10
+
11
+ desc "Install trigram text search module"
12
+ task :install_trigram => [:environment] do
13
+ installer = Texticle::PostgresModuleInstaller.new
14
+ installer.install_module('pg_trgm')
15
+
16
+ puts "Trigram text search module successfully installed into '#{installer.db_name}' database."
17
+ end
18
+ end
@@ -1,4 +1,4 @@
1
1
  database: texticle
2
- username: ncrespo
2
+ username: <username>
3
3
  pool: 5
4
4
  timeout: 5000
File without changes
@@ -0,0 +1,9 @@
1
+ require 'active_record'
2
+
3
+ class Game < ActiveRecord::Base
4
+ # string :system
5
+ # string :title
6
+ # text :description
7
+ end
8
+
9
+ class GameFail < Game; end
@@ -0,0 +1,9 @@
1
+ require 'active_record'
2
+
3
+ class WebComic < ActiveRecord::Base
4
+ # string :name
5
+ # string :author
6
+ # integer :id
7
+
8
+ has_many :characters
9
+ end
@@ -3,7 +3,76 @@ $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/../lib')
3
3
  require 'yaml'
4
4
  require 'texticle'
5
5
  require 'shoulda'
6
- require 'ruby-debug'
6
+ require 'pry'
7
+ require 'active_record'
8
+ require 'texticle'
9
+ require 'texticle/searchable'
7
10
 
8
11
  config = YAML.load_file File.expand_path(File.dirname(__FILE__) + '/config.yml')
9
12
  ActiveRecord::Base.establish_connection config.merge(:adapter => :postgresql)
13
+
14
+ class ARStandIn < ActiveRecord::Base;
15
+ self.abstract_class = true
16
+ extend Texticle
17
+ end
18
+
19
+ class NotThere < ARStandIn; end
20
+
21
+ class TexticleWebComic < 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 GameExtendedWithTexticle < Game
64
+ extend Texticle
65
+ end
66
+
67
+ class GameExtendedWithTexticleAndCustomLanguage < GameExtendedWithTexticle
68
+ def searchable_language
69
+ 'spanish'
70
+ end
71
+ end
72
+
73
+
74
+ class GameFail < Game; end
75
+
76
+ class GameFailExtendedWithTexticle < GameFail
77
+ extend Texticle
78
+ end
@@ -1,60 +1,91 @@
1
1
  require 'spec_helper'
2
- require 'fixtures/webcomic'
3
2
  require 'texticle/searchable'
4
3
 
5
4
  class SearchableTest < Test::Unit::TestCase
6
5
  context "when extending an ActiveRecord::Base subclass" do
7
- setup do
8
- @qcont = WebComic.create :name => "Questionable Content", :author => "Jeph Jaques"
9
- @jhony = WebComic.create :name => "Johnny Wander", :author => "Ananth & Yuko"
10
- @ddeeg = WebComic.create :name => "Dominic Deegan", :author => "Mookie"
11
- @penny = WebComic.create :name => "Penny Arcade", :author => "Tycho & Gabe"
12
- end
13
-
14
- teardown do
15
- WebComic.delete_all
16
- #Object.send(:remove_const, :WebComic) if defined?(WebComic)
17
- end
18
-
19
6
  context "with no parameters" do
20
7
  setup do
21
- WebComic.extend Searchable
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
22
16
  end
23
17
 
24
18
  should "search across all columns" do
25
- assert_equal [@penny], WebComic.search("Penny")
26
- assert_equal [@ddeeg], WebComic.search("Dominic")
19
+ assert_equal [@penny], WebComicWithSearchable.advanced_search("Penny")
20
+ assert_equal [@ddeeg], WebComicWithSearchable.advanced_search("Dominic")
27
21
  end
28
22
  end
29
23
 
30
24
  context "with one column as parameter" do
31
25
  setup do
32
- WebComic.extend Searchable(:name)
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
33
34
  end
34
35
 
35
36
  should "only search across the given column" do
36
- assert_equal [@penny], WebComic.search("Penny")
37
- assert_empty WebComic.search("Tycho")
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')
38
57
  end
39
58
 
40
59
  should "define :searchable_columns as private" do
41
- assert_raise(NoMethodError) { WebComic.searchable_columns }
60
+ assert_raise(NoMethodError) { WebComicWithSearchableName.searchable_columns }
42
61
  begin
43
- WebComic.searchable_columns
62
+ WebComicWithSearchableName.searchable_columns
44
63
  rescue NoMethodError => error
45
64
  assert_match error.message, /private method/
46
65
  end
47
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
48
72
  end
49
73
 
50
74
  context "with two columns as parameters" do
51
75
  setup do
52
- WebComic.extend Searchable(:name, :author)
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
53
84
  end
54
85
 
55
86
  should "only search across the given column" do
56
- assert_equal [@penny], WebComic.search("Penny")
57
- assert_equal [@penny], WebComic.search("Tycho")
87
+ assert_equal [@penny], WebComicWithSearchableNameAndAuthor.advanced_search("Penny")
88
+ assert_equal [@penny], WebComicWithSearchableNameAndAuthor.advanced_search("Tycho")
58
89
  end
59
90
  end
60
91
  end
@@ -1,18 +1,11 @@
1
1
  # coding: utf-8
2
2
  require 'spec_helper'
3
- require 'fixtures/webcomic'
4
- require 'fixtures/character'
5
- require 'fixtures/game'
6
3
 
7
4
  class TexticleTest < Test::Unit::TestCase
8
5
  context "after extending ActiveRecord::Base" do
9
- # before(:all)
10
- ActiveRecord::Base.extend(Texticle)
11
- class NotThere < ActiveRecord::Base; end
12
-
13
6
  should "not break #respond_to?" do
14
7
  assert_nothing_raised do
15
- ActiveRecord::Base.respond_to? :abstract_class?
8
+ ARStandIn.respond_to? :abstract_class?
16
9
  end
17
10
  end
18
11
 
@@ -24,9 +17,9 @@ class TexticleTest < Test::Unit::TestCase
24
17
  end
25
18
 
26
19
  should "not break #method_missing" do
27
- assert_raise(NoMethodError) { ActiveRecord::Base.random }
20
+ assert_raise(NoMethodError) { ARStandIn.random }
28
21
  begin
29
- ActiveRecord::Base.random
22
+ ARStandIn.random
30
23
  rescue NoMethodError => error
31
24
  assert_match error.message, /undefined method `random'/
32
25
  end
@@ -44,9 +37,9 @@ class TexticleTest < Test::Unit::TestCase
44
37
 
45
38
  context "when finding models based on searching a related model" do
46
39
  setup do
47
- @qc = WebComic.create :name => "Questionable Content", :author => "Jeph Jaques"
48
- @jw = WebComic.create :name => "Johnny Wander", :author => "Ananth & Yuko"
49
- @pa = WebComic.create :name => "Penny Arcade", :author => "Tycho & Gabe"
40
+ @qc = TexticleWebComic.create :name => "Questionable Content", :author => "Jeph Jaques"
41
+ @jw = TexticleWebComic.create :name => "Johnny Wander", :author => "Ananth & Yuko"
42
+ @pa = TexticleWebComic.create :name => "Penny Arcade", :author => "Tycho & Gabe"
50
43
 
51
44
  @gabe = @pa.characters.create :name => 'Gabe', :description => 'the simple one'
52
45
  @tycho = @pa.characters.create :name => 'Tycho', :description => 'the wordy one'
@@ -63,169 +56,161 @@ class TexticleTest < Test::Unit::TestCase
63
56
  end
64
57
 
65
58
  teardown do
66
- WebComic.delete_all
59
+ TexticleWebComic.delete_all
67
60
  Character.delete_all
68
61
  end
69
62
 
70
63
  should "look in the related model with nested searching syntax" do
71
- assert_equal [@jw], WebComic.joins(:characters).search(:characters => {:description => 'tall'})
72
- assert_equal [@pa, @jw, @qc].sort, WebComic.joins(:characters).search(:characters => {:description => 'anger'}).sort
73
- assert_equal [@pa, @qc].sort, WebComic.joins(:characters).search(:characters => {:description => 'crude'}).sort
64
+ assert_equal [@jw], TexticleWebComic.joins(:characters).advanced_search(:characters => {:description => 'tall'})
65
+ assert_equal [@pa, @jw, @qc].sort, TexticleWebComic.joins(:characters).advanced_search(:characters => {:description => 'anger'}).sort
66
+ assert_equal [@pa, @qc].sort, TexticleWebComic.joins(:characters).advanced_search(:characters => {:description => 'crude'}).sort
74
67
  end
75
68
  end
76
69
  end
77
70
 
78
71
  context "after extending an ActiveRecord::Base subclass" do
79
- # before(:all)
80
- class ::GameFail < Game; end
81
-
82
72
  setup do
83
- @zelda = Game.create :system => "NES", :title => "Legend of Zelda", :description => "A Link to the Past."
84
- @mario = Game.create :system => "NES", :title => "Super Mario Bros.", :description => "The original platformer."
85
- @sonic = Game.create :system => "Genesis", :title => "Sonic the Hedgehog", :description => "Spiky."
86
- @dkong = Game.create :system => "SNES", :title => "Diddy's Kong Quest", :description => "Donkey Kong Country 2"
87
- @megam = Game.create :system => nil, :title => "Mega Man", :description => "Beware Dr. Brain"
88
- @sfnes = Game.create :system => "SNES", :title => "Street Fighter 2", :description => "Yoga Flame!"
89
- @sfgen = Game.create :system => "Genesis", :title => "Street Fighter 2", :description => "Yoga Flame!"
90
- @takun = Game.create :system => "Saturn", :title => "Magical Tarurūto-kun", :description => "カッコイイ!"
73
+ @zelda = GameExtendedWithTexticle.create :system => "NES", :title => "Legend of Zelda", :description => "A Link to the Past."
74
+ @mario = GameExtendedWithTexticle.create :system => "NES", :title => "Super Mario Bros.", :description => "The original platformer."
75
+ @sonic = GameExtendedWithTexticle.create :system => "Genesis", :title => "Sonic the Hedgehog", :description => "Spiky."
76
+ @dkong = GameExtendedWithTexticle.create :system => "SNES", :title => "Diddy's Kong Quest", :description => "Donkey Kong Country 2"
77
+ @megam = GameExtendedWithTexticle.create :system => nil, :title => "Mega Man", :description => "Beware Dr. Brain"
78
+ @sfnes = GameExtendedWithTexticle.create :system => "SNES", :title => "Street Fighter 2", :description => "Yoga Flame!"
79
+ @sfgen = GameExtendedWithTexticle.create :system => "Genesis", :title => "Street Fighter 2", :description => "Yoga Flame!"
80
+ @takun = GameExtendedWithTexticle.create :system => "Saturn", :title => "Magical Tarurūto-kun", :description => "カッコイイ!"
91
81
  end
92
82
 
93
83
  teardown do
94
- Game.delete_all
84
+ GameExtendedWithTexticle.delete_all
95
85
  end
96
86
 
97
87
  should "not break respond_to? when connection is unavailable" do
98
- GameFail.establish_connection({:adapter => :postgresql, :database =>'unavailable', :username=>'bad', :pool=>5, :timeout=>5000}) rescue nil
88
+ GameFailExtendedWithTexticle.establish_connection({:adapter => :postgresql, :database =>'unavailable', :username=>'bad', :pool=>5, :timeout=>5000}) rescue nil
99
89
 
100
90
  assert_nothing_raised do
101
- GameFail.respond_to?(:search)
91
+ GameFailExtendedWithTexticle.respond_to?(:advanced_search)
102
92
  end
103
-
104
93
  end
105
94
 
106
95
  should "define a #search method" do
107
- assert Game.respond_to?(:search)
96
+ assert GameExtendedWithTexticle.respond_to?(:search)
108
97
  end
109
98
 
110
99
  context "when searching with a String argument" do
111
100
  should "search across all :string columns if no indexes have been specified" do
112
- assert_equal [@mario], Game.search("Mario")
113
- assert_equal Set.new([@mario, @zelda]), Game.search("NES").to_set
101
+ assert_equal [@mario], GameExtendedWithTexticle.advanced_search("Mario")
102
+ assert_equal Set.new([@mario, @zelda]), GameExtendedWithTexticle.advanced_search("NES").to_set
114
103
  end
115
104
 
116
105
  should "work if the query contains an apostrophe" do
117
- assert_equal [@dkong], Game.search("Diddy's")
106
+ assert_equal [@dkong], GameExtendedWithTexticle.advanced_search("Diddy's")
118
107
  end
119
108
 
120
109
  should "work if the query contains whitespace" do
121
- assert_equal [@megam], Game.search("Mega Man")
110
+ assert_equal [@megam], GameExtendedWithTexticle.advanced_search("Mega Man")
122
111
  end
123
112
 
124
113
  should "work if the query contains an accent" do
125
- assert_equal [@takun], Game.search("Tarurūto-kun")
114
+ assert_equal [@takun], GameExtendedWithTexticle.advanced_search("Tarurūto-kun")
126
115
  end
127
116
 
128
117
  should "search across records with NULL values" do
129
- assert_equal [@megam], Game.search("Mega")
118
+ assert_equal [@megam], GameExtendedWithTexticle.advanced_search("Mega")
130
119
  end
131
120
 
132
121
  should "scope consecutively" do
133
- assert_equal [@sfgen], Game.search("Genesis").search("Street Fighter")
122
+ assert_equal [@sfgen], GameExtendedWithTexticle.advanced_search("Genesis").advanced_search("Street Fighter")
134
123
  end
135
124
  end
136
125
 
137
126
  context "when searching with a Hash argument" do
138
127
  should "search across the given columns" do
139
- assert_empty Game.search(:title => "NES")
140
- assert_empty Game.search(:system => "Mario")
141
- assert_empty Game.search(:system => "NES", :title => "Sonic")
128
+ assert_empty GameExtendedWithTexticle.advanced_search(:title => "NES")
129
+ assert_empty GameExtendedWithTexticle.advanced_search(:system => "Mario")
130
+ assert_empty GameExtendedWithTexticle.advanced_search(:system => "NES", :title => "Sonic")
142
131
 
143
- assert_equal [@mario], Game.search(:title => "Mario")
132
+ assert_equal [@mario], GameExtendedWithTexticle.advanced_search(:title => "Mario")
144
133
 
145
- assert_equal 2, Game.search(:system => "NES").count
134
+ assert_equal 2, GameExtendedWithTexticle.advanced_search(:system => "NES").count
146
135
 
147
- assert_equal [@zelda], Game.search(:system => "NES", :title => "Zelda")
148
- assert_equal [@megam], Game.search(:title => "Mega")
136
+ assert_equal [@zelda], GameExtendedWithTexticle.advanced_search(:system => "NES", :title => "Zelda")
137
+ assert_equal [@megam], GameExtendedWithTexticle.advanced_search(:title => "Mega")
149
138
  end
150
139
 
151
140
  should "scope consecutively" do
152
- assert_equal [@sfgen], Game.search(:system => "Genesis").search(:title => "Street Fighter")
141
+ assert_equal [@sfgen], GameExtendedWithTexticle.advanced_search(:system => "Genesis").advanced_search(:title => "Street Fighter")
153
142
  end
154
143
 
155
144
  should "cast non-:string columns as text" do
156
- assert_equal [@mario], Game.search(:id => @mario.id)
145
+ assert_equal [@mario], GameExtendedWithTexticle.advanced_search(:id => @mario.id)
157
146
  end
158
147
  end
159
148
 
160
149
  context "when using dynamic search methods" do
161
150
  should "generate methods for each :string column" do
162
- assert_equal [@mario], Game.search_by_title("Mario")
163
- assert_equal [@takun], Game.search_by_system("Saturn")
151
+ assert_equal [@mario], GameExtendedWithTexticle.advanced_search_by_title("Mario")
152
+ assert_equal [@takun], GameExtendedWithTexticle.advanced_search_by_system("Saturn")
164
153
  end
165
154
 
166
155
  should "generate methods for each :text column" do
167
- assert_equal [@mario], Game.search_by_description("platform")
156
+ assert_equal [@mario], GameExtendedWithTexticle.advanced_search_by_description("platform")
168
157
  end
169
158
 
170
159
  should "generate methods for any combination of :string and :text columns" do
171
- assert_equal [@mario], Game.search_by_title_and_system("Mario", "NES")
172
- assert_equal [@sonic], Game.search_by_system_and_title("Genesis", "Sonic")
173
- assert_equal [@mario], Game.search_by_title_and_title("Mario", "Mario")
174
- assert_equal [@megam], Game.search_by_title_and_description("Man", "Brain")
160
+ assert_equal [@mario], GameExtendedWithTexticle.advanced_search_by_title_and_system("Mario", "NES")
161
+ assert_equal [@sonic], GameExtendedWithTexticle.advanced_search_by_system_and_title("Genesis", "Sonic")
162
+ assert_equal [@mario], GameExtendedWithTexticle.advanced_search_by_title_and_title("Mario", "Mario")
163
+ assert_equal [@megam], GameExtendedWithTexticle.advanced_search_by_title_and_description("Man", "Brain")
175
164
  end
176
165
 
177
166
  should "generate methods for inclusive searches" do
178
- assert_equal Set.new([@megam, @takun]), Game.search_by_system_or_title("Saturn", "Mega Man").to_set
167
+ assert_equal Set.new([@megam, @takun]), GameExtendedWithTexticle.advanced_search_by_system_or_title("Saturn", "Mega Man").to_set
179
168
  end
180
169
 
181
170
  should "scope consecutively" do
182
- assert_equal [@sfgen], Game.search_by_system("Genesis").search_by_title("Street Fighter")
171
+ assert_equal [@sfgen], GameExtendedWithTexticle.advanced_search_by_system("Genesis").advanced_search_by_title("Street Fighter")
183
172
  end
184
173
 
185
174
  should "generate methods for non-:string columns" do
186
- assert_equal [@mario], Game.search_by_id(@mario.id)
175
+ assert_equal [@mario], GameExtendedWithTexticle.advanced_search_by_id(@mario.id)
187
176
  end
188
177
 
189
178
  should "work with #respond_to?" do
190
- assert Game.respond_to?(:search_by_system)
191
- assert Game.respond_to?(:search_by_title)
192
- assert Game.respond_to?(:search_by_system_and_title)
193
- assert Game.respond_to?(:search_by_system_or_title)
194
- assert Game.respond_to?(:search_by_title_and_title_and_title)
195
- assert Game.respond_to?(:search_by_id)
179
+ assert GameExtendedWithTexticle.respond_to?(:advanced_search_by_system)
180
+ assert GameExtendedWithTexticle.respond_to?(:advanced_search_by_title)
181
+ assert GameExtendedWithTexticle.respond_to?(:advanced_search_by_system_and_title)
182
+ assert GameExtendedWithTexticle.respond_to?(:advanced_search_by_system_or_title)
183
+ assert GameExtendedWithTexticle.respond_to?(:advanced_search_by_title_and_title_and_title)
184
+ assert GameExtendedWithTexticle.respond_to?(:advanced_search_by_id)
196
185
 
197
- assert !Game.respond_to?(:search_by_title_and_title_or_title)
186
+ assert !GameExtendedWithTexticle.respond_to?(:advanced_search_by_title_and_title_or_title)
198
187
  end
199
188
 
200
189
  should "allow for 2 arguments to #respond_to?" do
201
- assert Game.respond_to?(:normalize, true)
190
+ assert GameExtendedWithTexticle.respond_to?(:normalize, true)
202
191
  end
203
192
  end
204
193
 
205
194
  context "when searching after selecting columns to return" do
206
195
  should "not fetch extra columns" do
207
196
  assert_raise(ActiveModel::MissingAttributeError) do
208
- Game.select(:title).search("Mario").first.system
197
+ GameExtendedWithTexticle.select(:title).advanced_search("Mario").first.system
209
198
  end
210
199
  end
211
200
  end
212
- end
213
-
214
- context "when setting a custom search language" do
215
- def Game.searchable_language
216
- 'spanish'
217
- end
218
201
 
219
- setup do
220
- Game.create :system => "PS3", :title => "Harry Potter & the Deathly Hallows"
221
- end
202
+ context "when setting a custom search language" do
203
+ setup do
204
+ GameExtendedWithTexticleAndCustomLanguage.create :system => "PS3", :title => "Harry Potter & the Deathly Hallows"
205
+ end
222
206
 
223
- teardown do
224
- Game.delete_all
225
- end
207
+ teardown do
208
+ GameExtendedWithTexticleAndCustomLanguage.delete_all
209
+ end
226
210
 
227
- should "still find results" do
228
- assert_not_empty Game.search_by_title("harry")
211
+ should "still find results" do
212
+ assert_not_empty GameExtendedWithTexticleAndCustomLanguage.advanced_search_by_title("harry")
213
+ end
229
214
  end
230
215
  end
231
216
  end
metadata CHANGED
@@ -1,20 +1,21 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: texticle
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.3
4
+ version: 2.1.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
8
+ - Ben Hamill
8
9
  - ecin
9
10
  - Aaron Patterson
10
11
  autorequire:
11
12
  bindir: bin
12
13
  cert_chain: []
13
- date: 2011-08-30 00:00:00.000000000 Z
14
+ date: 2013-01-12 00:00:00.000000000 Z
14
15
  dependencies:
15
16
  - !ruby/object:Gem::Dependency
16
17
  name: pg
17
- requirement: &70337748621180 !ruby/object:Gem::Requirement
18
+ requirement: !ruby/object:Gem::Requirement
18
19
  none: false
19
20
  requirements:
20
21
  - - ~>
@@ -22,10 +23,15 @@ dependencies:
22
23
  version: 0.11.0
23
24
  type: :development
24
25
  prerelease: false
25
- version_requirements: *70337748621180
26
+ version_requirements: !ruby/object:Gem::Requirement
27
+ none: false
28
+ requirements:
29
+ - - ~>
30
+ - !ruby/object:Gem::Version
31
+ version: 0.11.0
26
32
  - !ruby/object:Gem::Dependency
27
33
  name: shoulda
28
- requirement: &70337748620400 !ruby/object:Gem::Requirement
34
+ requirement: !ruby/object:Gem::Requirement
29
35
  none: false
30
36
  requirements:
31
37
  - - ~>
@@ -33,32 +39,63 @@ dependencies:
33
39
  version: 2.11.3
34
40
  type: :development
35
41
  prerelease: false
36
- version_requirements: *70337748620400
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ none: false
44
+ requirements:
45
+ - - ~>
46
+ - !ruby/object:Gem::Version
47
+ version: 2.11.3
37
48
  - !ruby/object:Gem::Dependency
38
49
  name: rake
39
- requirement: &70337748619540 !ruby/object:Gem::Requirement
50
+ requirement: !ruby/object:Gem::Requirement
40
51
  none: false
41
52
  requirements:
42
53
  - - ~>
43
54
  - !ruby/object:Gem::Version
44
- version: 0.8.0
55
+ version: 0.9.0
45
56
  type: :development
46
57
  prerelease: false
47
- version_requirements: *70337748619540
48
- - !ruby/object:Gem::Dependency
49
- name: ruby-debug19
50
- requirement: &70337748618760 !ruby/object:Gem::Requirement
58
+ version_requirements: !ruby/object:Gem::Requirement
51
59
  none: false
52
60
  requirements:
53
61
  - - ~>
54
62
  - !ruby/object:Gem::Version
55
- version: 0.11.6
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'
56
72
  type: :development
57
73
  prerelease: false
58
- version_requirements: *70337748618760
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'
59
96
  - !ruby/object:Gem::Dependency
60
97
  name: activerecord
61
- requirement: &70337748617780 !ruby/object:Gem::Requirement
98
+ requirement: !ruby/object:Gem::Requirement
62
99
  none: false
63
100
  requirements:
64
101
  - - ~>
@@ -66,10 +103,16 @@ dependencies:
66
103
  version: '3.0'
67
104
  type: :runtime
68
105
  prerelease: false
69
- version_requirements: *70337748617780
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ none: false
108
+ requirements:
109
+ - - ~>
110
+ - !ruby/object:Gem::Version
111
+ version: '3.0'
70
112
  description: ! "Texticle exposes full text search capabilities from PostgreSQL, extending\n
71
113
  \ ActiveRecord with scopes making search easy and fun!"
72
114
  email:
115
+ - git-commits@benhamill.com
73
116
  - ecin@copypastel.com
74
117
  executables: []
75
118
  extensions: []
@@ -79,18 +122,26 @@ extra_rdoc_files:
79
122
  - README.rdoc
80
123
  files:
81
124
  - CHANGELOG.rdoc
125
+ - Gemfile
82
126
  - Manifest.txt
83
127
  - README.rdoc
84
128
  - Rakefile
85
129
  - lib/texticle.rb
86
- - lib/texticle/searchable.rb
130
+ - lib/texticle/full_text_indexer.rb
87
131
  - lib/texticle/rails.rb
132
+ - lib/texticle/searchable.rb
133
+ - lib/texticle/tasks.rb
134
+ - lib/texticle/postgres_module_installer.rb
135
+ - spec/config.yml.example
136
+ - spec/fixtures/character.rb
137
+ - spec/fixtures/game.rb
138
+ - spec/fixtures/webcomic.rb
88
139
  - spec/spec_helper.rb
89
- - spec/texticle_spec.rb
90
140
  - spec/texticle/searchable_spec.rb
91
- - spec/config.yml
92
- homepage: http://tenderlove.github.com/texticle
93
- licenses: []
141
+ - spec/texticle_spec.rb
142
+ homepage: http://texticle.github.com/texticle
143
+ licenses:
144
+ - MIT
94
145
  post_install_message:
95
146
  rdoc_options:
96
147
  - --main
@@ -110,12 +161,17 @@ required_rubygems_version: !ruby/object:Gem::Requirement
110
161
  - !ruby/object:Gem::Version
111
162
  version: '0'
112
163
  requirements: []
113
- rubyforge_project: texticle
114
- rubygems_version: 1.8.10
164
+ rubyforge_project:
165
+ rubygems_version: 1.8.23
115
166
  signing_key:
116
167
  specification_version: 3
117
168
  summary: Texticle exposes full text search capabilities from PostgreSQL
118
169
  test_files:
170
+ - spec/config.yml.example
171
+ - spec/fixtures/character.rb
172
+ - spec/fixtures/game.rb
173
+ - spec/fixtures/webcomic.rb
119
174
  - spec/spec_helper.rb
175
+ - spec/texticle/searchable_spec.rb
120
176
  - spec/texticle_spec.rb
121
- - spec/config.yml
177
+ has_rdoc: