texticle 2.0.3 → 2.1.0

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