minidusen 0.7.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.
@@ -0,0 +1,25 @@
1
+ module Minidusen
2
+ class Token
3
+
4
+ attr_reader :field, :value, :exclude
5
+
6
+ def initialize(options)
7
+ @value = options.fetch(:value)
8
+ @exclude = options.fetch(:exclude)
9
+ @field = options.fetch(:field).to_s
10
+ end
11
+
12
+ def to_s
13
+ value
14
+ end
15
+
16
+ def text?
17
+ field == 'text'
18
+ end
19
+
20
+ def exclude?
21
+ exclude
22
+ end
23
+
24
+ end
25
+ end
@@ -0,0 +1,55 @@
1
+ module Minidusen
2
+ module Util
3
+ extend self
4
+
5
+ def postgres?(scope)
6
+ adapter_name = scope.connection.class.name
7
+ adapter_name =~ /postgres/i
8
+ end
9
+
10
+ def like_expression(phrase)
11
+ "%#{escape_for_like_query(phrase)}%"
12
+ end
13
+
14
+ def ilike_operator(scope)
15
+ if postgres?(scope)
16
+ 'ILIKE'
17
+ else
18
+ 'LIKE'
19
+ end
20
+ end
21
+
22
+ def regexp_operator(scope)
23
+ if postgres?(scope)
24
+ '~'
25
+ else
26
+ 'REGEXP'
27
+ end
28
+ end
29
+
30
+ def escape_with_backslash(phrase, characters)
31
+ characters << '\\'
32
+ pattern = /[#{characters.collect(&Regexp.method(:quote)).join('')}]/
33
+ # debugger
34
+ phrase.gsub(pattern) do |match|
35
+ "\\#{match}"
36
+ end
37
+ end
38
+
39
+ def escape_for_like_query(phrase)
40
+ # phrase.gsub("%", "\\%").gsub("_", "\\_")
41
+ escape_with_backslash(phrase, ['%', '_'])
42
+ end
43
+
44
+ def qualify_column_name(model, column_name)
45
+ column_name = column_name.to_s
46
+ unless column_name.include?('.')
47
+ quoted_table_name = model.connection.quote_table_name(model.table_name)
48
+ quoted_column_name = model.connection.quote_column_name(column_name)
49
+ column_name = "#{quoted_table_name}.#{quoted_column_name}"
50
+ end
51
+ column_name
52
+ end
53
+
54
+ end
55
+ end
@@ -0,0 +1,3 @@
1
+ module Minidusen
2
+ VERSION = '0.7.0'
3
+ end
data/minidusen.gemspec ADDED
@@ -0,0 +1,23 @@
1
+ $:.push File.expand_path("../lib", __FILE__)
2
+ require "minidusen/version"
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = 'minidusen'
6
+ s.version = Minidusen::VERSION
7
+ s.authors = ["Henning Koch"]
8
+ s.email = 'henning.koch@makandra.de'
9
+ s.homepage = 'https://github.com/makandra/minidusen'
10
+ s.summary = 'Low-tech search for ActiveRecord with MySQL or PostgreSQL'
11
+ s.description = s.summary
12
+ s.license = 'MIT'
13
+
14
+ s.files = `git ls-files`.split("\n").reject { |path| File.lstat(path).symlink? }
15
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n").reject { |path| File.lstat(path).symlink? }
16
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
17
+ s.require_paths = ["lib"]
18
+
19
+ s.add_dependency('activesupport', '>=3.2')
20
+ s.add_dependency('activerecord', '>=3.2')
21
+ s.add_dependency('edge_rider', '>=0.2.5')
22
+
23
+ end
@@ -0,0 +1,45 @@
1
+ describe ActiveRecord::Base do
2
+
3
+ describe '.where_like' do
4
+
5
+ it 'matches a record if a word appears in any of the given columns' do
6
+ match1 = User.create!(:name => 'word', :city => 'XXXX')
7
+ match2 = User.create!(:name => 'XXXX', :city => 'word')
8
+ no_match = User.create!(:name => 'XXXX', :city => 'XXXX')
9
+ User.where_like([:name, :city] => 'word').to_a.should =~ [match1, match2]
10
+ end
11
+
12
+ it 'matches a record if it contains all the given words' do
13
+ match1 = User.create!(:city => 'word1 word2')
14
+ match2 = User.create!(:city => 'word2 word1')
15
+ no_match = User.create!(:city => 'word1')
16
+ User.where_like(:city => ['word1', 'word2']).to_a.should =~ [match1, match2]
17
+ end
18
+
19
+ describe 'with :negate option' do
20
+
21
+ it 'rejects a record if a word appears in any of the given columns' do
22
+ no_match1 = User.create!(:name => 'word', :city => 'XXXX')
23
+ no_match2 = User.create!(:name => 'XXXX', :city => 'word')
24
+ match = User.create!(:name => 'XXXX', :city => 'XXXX')
25
+ User.where_like({ [:name, :city] => 'word' }, :negate => true).to_a.should =~ [match]
26
+ end
27
+
28
+ it 'rejects a record if it matches at least one of the given words' do
29
+ no_match1 = User.create!(:city => 'word1')
30
+ no_match2 = User.create!(:city => 'word2')
31
+ match = User.create!(:city => 'word3')
32
+ User.where_like({ :city => ['word1', 'word2'] }, :negate => true).to_a.should =~ [match]
33
+ end
34
+
35
+ it "doesn't match NULL values" do
36
+ no_match = User.create!(:city => nil)
37
+ match = User.create!(:city => 'word3')
38
+ User.where_like({ :city => ['word1'] }, :negate => true).to_a.should =~ [match]
39
+ end
40
+
41
+ end
42
+
43
+ end
44
+
45
+ end
@@ -0,0 +1,196 @@
1
+ describe Minidusen::Filter do
2
+
3
+ let :user_filter do
4
+ UserFilter.new
5
+ end
6
+
7
+ let :recipe_filter do
8
+ RecipeFilter.new
9
+ end
10
+
11
+ describe '#filter' do
12
+
13
+ it 'should find records by given words' do
14
+ match = User.create!(:name => 'Abraham')
15
+ no_match = User.create!(:name => 'Elizabath')
16
+ user_filter.filter(User, 'Abraham').to_a.should == [match]
17
+ end
18
+
19
+ it 'should make a case-insensitive search' do
20
+ match = User.create!(:name => 'Abraham')
21
+ no_match = User.create!(:name => 'Elizabath')
22
+ user_filter.filter(User, 'aBrAhAm').to_a.should == [match]
23
+ end
24
+
25
+ it 'should not find stale text after fields were updated (bugfix)' do
26
+ match = User.create!(:name => 'Abraham')
27
+ no_match = User.create!(:name => 'Elizabath')
28
+ match.update_attributes!(:name => 'Johnny')
29
+ user_filter.filter(User, 'Abraham').to_a.should be_empty
30
+ user_filter.filter(User, 'Johnny').to_a.should == [match]
31
+ end
32
+
33
+ it 'should AND multiple words' do
34
+ match = User.create!(:name => 'Abraham Lincoln')
35
+ no_match = User.create!(:name => 'Abraham')
36
+ user_filter.filter(User, 'Abraham Lincoln').to_a.should == [match]
37
+ end
38
+
39
+ it 'should find records by phrases' do
40
+ match = User.create!(:name => 'Abraham Lincoln')
41
+ no_match = User.create!(:name => 'Abraham John Lincoln')
42
+ user_filter.filter(User, '"Abraham Lincoln"').to_a.should == [match]
43
+ end
44
+
45
+ it 'should find records by qualified fields' do
46
+ match = User.create!(:name => 'foo@bar.com', :email => 'foo@bar.com')
47
+ no_match = User.create!(:name => 'foo@bar.com', :email => 'bam@baz.com')
48
+ user_filter.filter(User, 'email:foo@bar.com').to_a.should == [match]
49
+ end
50
+
51
+ it 'should find no records if a nonexistent qualifier is used' do
52
+ User.create!(:name => 'someuser', :email => 'foo@bar.com')
53
+ user_filter.filter(User, 'nonexistent_qualifier:someuser email:foo@bar.com').to_a.should == []
54
+ end
55
+
56
+ it 'should allow phrases as values for qualified field queries' do
57
+ match = User.create!(:name => 'Foo Bar', :city => 'Foo Bar')
58
+ no_match = User.create!(:name => 'Foo Bar', :city => 'Bar Foo')
59
+ user_filter.filter(User, 'city:"Foo Bar"').to_a.should == [match]
60
+ end
61
+
62
+ it 'should allow to mix multiple types of tokens in a single query' do
63
+ match = User.create!(:name => 'Abraham', :city => 'Foohausen')
64
+ no_match = User.create!(:name => 'Abraham', :city => 'Barhausen')
65
+ user_filter.filter(User, 'Foo city:Foohausen').to_a.should == [match]
66
+ end
67
+
68
+ it 'should not find records from another model' do
69
+ match = User.create!(:name => 'Abraham')
70
+ Recipe.create!(:name => 'Abraham')
71
+ user_filter.filter(User, 'Abraham').to_a.should == [match]
72
+ end
73
+
74
+ it 'should find words where one letter is separated from other letters by a period' do
75
+ match = User.create!(:name => 'E.ONNNEN')
76
+ user_filter.filter(User, 'E.ONNNEN').to_a.should == [match]
77
+ end
78
+
79
+ it 'should find words where one letter is separated from other letters by a semicolon' do
80
+ match = User.create!(:name => 'E;ONNNEN')
81
+ user_filter.filter(User, 'E;ONNNEN')
82
+ user_filter.filter(User, 'E;ONNNEN').to_a.should == [match]
83
+ end
84
+
85
+ it 'should distinguish between "Baden" and "Baden-Baden" (bugfix)' do
86
+ match = User.create!(:city => 'Baden-Baden')
87
+ no_match = User.create!(:city => 'Baden')
88
+ user_filter.filter(User, 'Baden-Baden').to_a.should == [match]
89
+ end
90
+
91
+ it 'should handle umlauts and special characters' do
92
+ match = User.create!(:city => 'púlvérìsätëûr')
93
+ user_filter.filter(User, 'púlvérìsätëûr').to_a.should == [match]
94
+ end
95
+
96
+ context 'with excludes' do
97
+
98
+ it 'should exclude words with prefix - (minus)' do
99
+ match = User.create!(:name => 'Sunny Flower')
100
+ no_match = User.create!(:name => 'Sunny Power')
101
+ no_match2 = User.create!(:name => 'Absolutly no match')
102
+ user_filter.filter(User, 'Sunny -Power').to_a.should == [match]
103
+ end
104
+
105
+ it 'should exclude phrases with prefix - (minus)' do
106
+ match = User.create!(:name => 'Buch Tastatur Schreibtisch')
107
+ no_match = User.create!(:name => 'Buch Schreibtisch Tastatur')
108
+ no_match2 = User.create!(:name => 'Absolutly no match')
109
+ user_filter.filter(User, 'Buch -"Schreibtisch Tastatur"').to_a.should == [match]
110
+ end
111
+
112
+ it 'should exclude qualified fields with prefix - (minus)' do
113
+ match = User.create!(:name => 'Abraham', :city => 'Foohausen')
114
+ no_match = User.create!(:name => 'Abraham', :city => 'Barhausen')
115
+ no_match2 = User.create!(:name => 'Absolutly no match')
116
+ user_filter.filter(User, 'Abraham -city:Barhausen').to_a.should == [match]
117
+ end
118
+
119
+ it 'should work if the query only contains excluded words' do
120
+ match = User.create!(:name => 'Sunny Flower')
121
+ no_match = User.create!(:name => 'Sunny Power')
122
+ user_filter.filter(User, '-Power').to_a.should == [match]
123
+ end
124
+
125
+ it 'should work if the query only contains excluded phrases' do
126
+ match = User.create!(:name => 'Buch Tastatur Schreibtisch')
127
+ no_match = User.create!(:name => 'Buch Schreibtisch Tastatur')
128
+ user_filter.filter(User, '-"Schreibtisch Tastatur"').to_a.should == [match]
129
+ end
130
+
131
+ it 'should work if the query only contains excluded qualified fields' do
132
+ match = User.create!(:name => 'Abraham', :city => 'Foohausen')
133
+ no_match = User.create!(:name => 'Abraham', :city => 'Barhausen')
134
+ user_filter.filter(User, '-city:Barhausen').to_a.should == [match]
135
+ end
136
+
137
+ it 'respects an existing scope chain when there are only excluded tokens (bugfix)' do
138
+ match = User.create!(:name => 'Abraham', :city => 'Foohausen')
139
+ no_match = User.create!(:name => 'Abraham', :city => 'Barhausen')
140
+ also_no_match = User.create!(:name => 'Bebraham', :city => 'Foohausen')
141
+ user_scope = User.scoped(:conditions => { :name => 'Abraham' })
142
+ user_filter.filter(user_scope, '-Barhausen').to_a.should == [match]
143
+ end
144
+
145
+ it 'should work if there are fields contained in the search that are NULL' do
146
+ match = User.create!(:name => 'Sunny Flower', :city => nil, :email => nil)
147
+ no_match = User.create!(:name => 'Sunny Power', :city => nil, :email => nil)
148
+ no_match2 = User.create!(:name => 'Absolutly no match')
149
+ user_filter.filter(User, 'Sunny -Power').to_a.should == [match]
150
+ end
151
+
152
+ it 'should work if search_by contains a join (bugfix)' do
153
+ category1 = Recipe::Category.create!(:name => 'Rice')
154
+ category2 = Recipe::Category.create!(:name => 'Barbecue')
155
+ match = Recipe.create!(:name => 'Martini Chicken', :category => category1)
156
+ no_match = Recipe.create!(:name => 'Barbecue Chicken', :category => category2)
157
+ recipe_filter.filter(Recipe, 'Chicken -category:Barbecue').to_a.should == [match]
158
+ end
159
+
160
+ it 'should work when search_by uses SQL-Regexes which need to be "and"ed together by syntax#build_exclude_scope (bugfix)' do
161
+ match = User.create!(:name => 'Sunny Flower', :city => "Flower")
162
+ no_match = User.create!(:name => 'Sunny Power', :city => "Power")
163
+ user_filter.filter(User, '-name_and_city_regex:Power').to_a.should == [match]
164
+ end
165
+
166
+ end
167
+
168
+ context 'when the given query is blank' do
169
+
170
+ it 'returns all records' do
171
+ match = User.create!
172
+ user_filter.filter(User, '').scoped.to_a.should == [match]
173
+ end
174
+
175
+ it 'respects an existing scope chain' do
176
+ match = User.create!(:name => 'Abraham')
177
+ no_match = User.create!(:name => 'Elizabath')
178
+ scope = User.scoped(:conditions => { :name => 'Abraham' })
179
+ user_filter.filter(scope, '').scoped.to_a.should == [match]
180
+ end
181
+
182
+ end
183
+
184
+ end
185
+
186
+ describe '#minidusen_syntax' do
187
+
188
+ it "should return the model's syntax definition" do
189
+ syntax = UserFilter.send(:minidusen_syntax)
190
+ syntax.should be_a(Minidusen::Syntax)
191
+ syntax.fields.keys.should =~ ['text', 'email', 'city', 'role', 'name_and_city_regex']
192
+ end
193
+
194
+ end
195
+
196
+ end
@@ -0,0 +1,22 @@
1
+ describe Minidusen::Parser do
2
+
3
+ describe '.parse' do
4
+
5
+ it 'should parse field tokens first, because they usually give maximum filtering at little cost' do
6
+ query = Minidusen::Parser.parse('word1 field1:field1-value word2 field2:field2-value')
7
+ query.collect(&:value).should == ['field1-value', 'field2-value', 'word1', 'word2']
8
+ end
9
+
10
+ it 'should not consider the dash to be a word boundary' do
11
+ query = Minidusen::Parser.parse('Baden-Baden')
12
+ query.collect(&:value).should == ['Baden-Baden']
13
+ end
14
+
15
+ it 'should parse umlauts and accents' do
16
+ query = Minidusen::Parser.parse('field:åöÙÔøüéíÁ "ÄüÊçñÆ ððÿáÒÉ" pulvérisateur pędzić')
17
+ query.collect(&:value).should == ['åöÙÔøüéíÁ', 'ÄüÊçñÆ ððÿáÒÉ', 'pulvérisateur', 'pędzić']
18
+ end
19
+
20
+ end
21
+
22
+ end
@@ -0,0 +1,18 @@
1
+ describe Minidusen::Query do
2
+
3
+ describe '#condensed' do
4
+
5
+ it 'should return a version of the query where all text tokens have been collapsed into a single token with an Array value' do
6
+ query = Minidusen::Parser.parse('field:value foo bar baz')
7
+ query.tokens.size.should == 4
8
+ condensed_query = query.condensed
9
+ condensed_query.tokens.size.should == 2
10
+ condensed_query[0].field.should == 'field'
11
+ condensed_query[0].value.should == 'value'
12
+ condensed_query[1].field.should == 'text'
13
+ condensed_query[1].value.should == ['foo', 'bar', 'baz']
14
+ end
15
+
16
+ end
17
+
18
+ end
@@ -0,0 +1,3 @@
1
+ describe Minidusen::Util do
2
+
3
+ end
@@ -0,0 +1,29 @@
1
+ $: << File.join(File.dirname(__FILE__), "/../../lib" )
2
+
3
+ require 'minidusen'
4
+ require 'byebug'
5
+
6
+ ActiveRecord::Base.default_timezone = :local
7
+
8
+ Dir["#{File.dirname(__FILE__)}/support/*.rb"].sort.each {|f| require f}
9
+ Dir["#{File.dirname(__FILE__)}/shared_examples/*.rb"].sort.each {|f| require f}
10
+
11
+
12
+ RSpec.configure do |config|
13
+
14
+ config.expect_with(:rspec) { |c| c.syntax = [:should, :expect] }
15
+
16
+ config.around do |example|
17
+ if example.metadata.fetch(:rollback, true)
18
+ ActiveRecord::Base.transaction do
19
+ begin
20
+ example.run
21
+ ensure
22
+ raise ActiveRecord::Rollback
23
+ end
24
+ end
25
+ else
26
+ example.run
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,50 @@
1
+ require 'yaml'
2
+
3
+ database_config_file = ENV['TRAVIS'] ? 'database.travis.yml' : 'database.yml'
4
+ database_config_file = File.join(File.dirname(__FILE__), database_config_file)
5
+ File.exists?(database_config_file) or raise "Missing database configuration file: #{database_config_file}"
6
+
7
+ database_config = YAML.load_file(database_config_file)
8
+
9
+ connection_config = {}
10
+
11
+ case ENV['BUNDLE_GEMFILE']
12
+ when /pg/, /postgres/
13
+ connection_config = database_config['postgresql'].merge(adapter: 'postgresql')
14
+ when /mysql2/
15
+ connection_config = database_config['mysql'].merge(adapter: 'mysql2', encoding: 'utf8')
16
+ else
17
+ raise "Unknown database type in Gemfile suffix: #{ENV['BUNDLE_GEMFILE']}"
18
+ end
19
+
20
+ ActiveRecord::Base.establish_connection(connection_config)
21
+
22
+
23
+ connection = ::ActiveRecord::Base.connection
24
+ connection.tables.each do |table|
25
+ connection.drop_table table
26
+ end
27
+
28
+ ActiveRecord::Migration.class_eval do
29
+
30
+ create_table :users do |t|
31
+ t.string :name
32
+ t.string :email
33
+ t.string :city
34
+ end
35
+
36
+ create_table :recipes do |t|
37
+ t.string :name
38
+ t.integer :category_id
39
+ end
40
+
41
+ create_table :recipe_ingredients do |t|
42
+ t.string :name
43
+ t.integer :recipe_id
44
+ end
45
+
46
+ create_table :recipe_categories do |t|
47
+ t.string :name
48
+ end
49
+
50
+ end