attr_searchable 0.0.3 → 0.0.4
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.
- data/README.md +68 -0
- data/lib/attr_searchable.rb +20 -9
- data/lib/attr_searchable/version.rb +1 -1
- data/lib/attr_searchable_grammar.rb +2 -2
- data/lib/attr_searchable_grammar/attributes.rb +31 -8
- data/test/date_test.rb +75 -0
- data/test/test_helper.rb +8 -3
- metadata +4 -2
data/README.md
CHANGED
@@ -5,6 +5,8 @@
|
|
5
5
|
[](https://gemnasium.com/mrkamel/attr_searchable)
|
6
6
|
[](http://badge.fury.io/rb/attr_searchable)
|
7
7
|
|
8
|
+

|
9
|
+
|
8
10
|
AttrSearchable extends your ActiveRecord models to support fulltext search
|
9
11
|
engine like queries via simple query strings and hash-based queries. Assume you
|
10
12
|
have a `Book` model having various attributes like `title`, `author`, `stock`,
|
@@ -218,6 +220,28 @@ from from the usual non-fulltext indices. Thus, you should add a usual index on
|
|
218
220
|
every column you expose to search queries plus a fulltext index for every
|
219
221
|
fulltext attribute.
|
220
222
|
|
223
|
+
In case you can't use fulltext indices, because you're e.g. still on MySQL 5.5
|
224
|
+
while using InnoDB or another RDBMS without fulltext support, you can make your
|
225
|
+
RDBMS use usual non-fulltext indices for string columns if you don't need the
|
226
|
+
left wildcard within `LIKE` queries. Simply supply the following option:
|
227
|
+
|
228
|
+
```ruby
|
229
|
+
class User < ActiveRecord::Base
|
230
|
+
include AttrSearchable
|
231
|
+
|
232
|
+
attr_searchable :username
|
233
|
+
attr_searchable_options :username, :left_wildcard => false
|
234
|
+
|
235
|
+
# ...
|
236
|
+
```
|
237
|
+
|
238
|
+
such that AttrSearchable will omit the left most wildcard.
|
239
|
+
|
240
|
+
```ruby
|
241
|
+
User.search("admin")
|
242
|
+
# ... WHERE users.username LIKE 'admin%'
|
243
|
+
```
|
244
|
+
|
221
245
|
## Associations
|
222
246
|
|
223
247
|
If you specify searchable attributes from another model, like
|
@@ -267,6 +291,44 @@ end
|
|
267
291
|
AttrSearchable will then skip any association auto loading and will use
|
268
292
|
the `search_scope` instead.
|
269
293
|
|
294
|
+
## Custom table names and associations
|
295
|
+
|
296
|
+
AttrSearchable tries to infer a model's class name and SQL alias from the
|
297
|
+
specified attributes to autodetect datatype definitions, etc. This usually
|
298
|
+
works quite fine. In case you're using custom table names via `self.table_name
|
299
|
+
= ...` or if a model is associated multiple times, AttrSearchable however can't
|
300
|
+
infer the class and alias names, e.g.
|
301
|
+
|
302
|
+
```ruby
|
303
|
+
class Book < ActiveRecord::Base
|
304
|
+
# ...
|
305
|
+
|
306
|
+
has_many :users, :through => :comments
|
307
|
+
belongs_to :user
|
308
|
+
|
309
|
+
attr_searchable :user => ["users.username", "users_books.username"]
|
310
|
+
|
311
|
+
# ...
|
312
|
+
end
|
313
|
+
```
|
314
|
+
|
315
|
+
Here, for queries to work you have to use `users_books.username`, because
|
316
|
+
ActiveRecord assigns a different SQL alias for users within its SQL queries,
|
317
|
+
because the user model is associated multiple times. However, as AttrSearchable
|
318
|
+
now can't infer the `User` model from `users_books`, you have to add:
|
319
|
+
|
320
|
+
```ruby
|
321
|
+
class Book < ActiveRecord::Base
|
322
|
+
# ...
|
323
|
+
|
324
|
+
attr_searchable_alias :users_books => :user
|
325
|
+
|
326
|
+
# ...
|
327
|
+
end
|
328
|
+
```
|
329
|
+
|
330
|
+
to tell AttrSearchable about the custom SQL alias and mapping.
|
331
|
+
|
270
332
|
## Supported operators
|
271
333
|
|
272
334
|
Query string queries support `AND/and`, `OR/or`, `:`, `=`, `!=`, `<`, `<=`,
|
@@ -360,6 +422,12 @@ Book.unsafe_search("stock: None") # => raise AttrSearchable::IncompatibleDatatyp
|
|
360
422
|
|
361
423
|
## Changelog
|
362
424
|
|
425
|
+
Version 0.0.4:
|
426
|
+
|
427
|
+
* Fixed date attributes
|
428
|
+
* Fail softly for mixed datatype attributes
|
429
|
+
* Support custom table, class and alias names via attr_searchable_alias
|
430
|
+
|
363
431
|
Version 0.0.3:
|
364
432
|
|
365
433
|
* belongs_to association fixes
|
data/lib/attr_searchable.rb
CHANGED
@@ -18,8 +18,8 @@ module AttrSearchable
|
|
18
18
|
class ParseError < RuntimeError; end
|
19
19
|
|
20
20
|
module Parser
|
21
|
-
def self.parse(
|
22
|
-
|
21
|
+
def self.parse(query, model)
|
22
|
+
query.is_a?(Hash) ? parse_hash(query, model) : parse_string(query, model)
|
23
23
|
end
|
24
24
|
|
25
25
|
def self.parse_hash(hash, model)
|
@@ -40,6 +40,9 @@ module AttrSearchable
|
|
40
40
|
base.class_attribute :searchable_attribute_options
|
41
41
|
base.searchable_attribute_options = {}
|
42
42
|
|
43
|
+
base.class_attribute :searchable_attribute_aliases
|
44
|
+
base.searchable_attribute_aliases = {}
|
45
|
+
|
43
46
|
base.extend ClassMethods
|
44
47
|
end
|
45
48
|
|
@@ -61,22 +64,30 @@ module AttrSearchable
|
|
61
64
|
end
|
62
65
|
|
63
66
|
def attr_searchable_options(key, options = {})
|
64
|
-
self.searchable_attribute_options[key.to_s] = (
|
67
|
+
self.searchable_attribute_options[key.to_s] = (searchable_attribute_options[key.to_s] || {}).merge(options)
|
68
|
+
end
|
69
|
+
|
70
|
+
def attr_searchable_alias(hash)
|
71
|
+
hash.each do |key, value|
|
72
|
+
self.searchable_attribute_aliases[key.to_s] = value.respond_to?(:table_name) ? value.table_name : value.to_s
|
73
|
+
end
|
65
74
|
end
|
66
75
|
|
67
|
-
def search(
|
68
|
-
unsafe_search
|
76
|
+
def search(query)
|
77
|
+
unsafe_search query
|
69
78
|
rescue AttrSearchable::RuntimeError
|
70
79
|
respond_to?(:none) ? none : where("1 = 0")
|
71
80
|
end
|
72
81
|
|
73
|
-
def unsafe_search(
|
74
|
-
return respond_to?(:scoped) ? scoped : all if
|
82
|
+
def unsafe_search(query)
|
83
|
+
return respond_to?(:scoped) ? scoped : all if query.blank?
|
84
|
+
|
85
|
+
associations = searchable_attributes.values.flatten.uniq.collect { |column| column.split(".").first }.collect { |column| searchable_attribute_aliases[column] || column.to_sym }
|
75
86
|
|
76
87
|
scope = respond_to?(:search_scope) ? search_scope : nil
|
77
|
-
scope ||= eager_load(
|
88
|
+
scope ||= eager_load(associations - [name.tableize.to_sym])
|
78
89
|
|
79
|
-
scope.where AttrSearchable::Parser.parse(
|
90
|
+
scope.where AttrSearchable::Parser.parse(query, self).optimize!.to_sql(self)
|
80
91
|
end
|
81
92
|
end
|
82
93
|
end
|
@@ -87,9 +87,9 @@ module AttrSearchableGrammar
|
|
87
87
|
keys = model.searchable_attribute_options.select { |key, value| value[:default] == true }.keys
|
88
88
|
keys = model.searchable_attributes.keys if keys.empty?
|
89
89
|
|
90
|
-
queries = keys.collect { |key| collection_for key }.select { |
|
90
|
+
queries = keys.collect { |key| collection_for key }.select { |collection| collection.compatible? text_value }.collect { |collection| collection.matches text_value }
|
91
91
|
|
92
|
-
raise AttrSearchable::NoSearchableAttributes
|
92
|
+
raise AttrSearchable::NoSearchableAttributes if queries.empty?
|
93
93
|
|
94
94
|
queries.flatten.inject(:or)
|
95
95
|
end
|
@@ -55,13 +55,24 @@ module AttrSearchableGrammar
|
|
55
55
|
@attributes ||= model.searchable_attributes[key].collect { |attribute_definition| attribute_for attribute_definition }
|
56
56
|
end
|
57
57
|
|
58
|
+
def klass_for(table)
|
59
|
+
klass = model.searchable_attribute_aliases[table]
|
60
|
+
klass ||= table
|
61
|
+
|
62
|
+
klass.classify.constantize
|
63
|
+
end
|
64
|
+
|
65
|
+
def alias_for(table)
|
66
|
+
(model.searchable_attribute_aliases[table] && table) || klass_for(table).table_name
|
67
|
+
end
|
68
|
+
|
58
69
|
def attribute_for(attribute_definition)
|
59
70
|
table, column = attribute_definition.split(".")
|
60
|
-
klass = table
|
71
|
+
klass = klass_for(table)
|
61
72
|
|
62
73
|
raise(AttrSearchable::UnknownAttribute, "Unknown attribute #{attribute_definition}") unless klass.columns_hash[column]
|
63
74
|
|
64
|
-
Attributes.const_get(klass.columns_hash[column].type.to_s.classify).new(klass.arel_table.alias(
|
75
|
+
Attributes.const_get(klass.columns_hash[column].type.to_s.classify).new(klass.arel_table.alias(alias_for(table))[column], klass, options)
|
65
76
|
end
|
66
77
|
end
|
67
78
|
|
@@ -148,16 +159,16 @@ module AttrSearchableGrammar
|
|
148
159
|
::Time.new($1, $3, 15).beginning_of_month .. ::Time.new($1, $3, 15).end_of_month
|
149
160
|
elsif value =~ /^([0-9]{1,2})(\.|-|\/)([0-9]{4,})$/
|
150
161
|
::Time.new($3, $1, 15).beginning_of_month .. ::Time.new($3, $1, 15).end_of_month
|
151
|
-
elsif value !~ /:/
|
162
|
+
elsif value !~ /:/
|
152
163
|
time = ::Time.parse(value)
|
153
164
|
time.beginning_of_day .. time.end_of_day
|
154
165
|
else
|
155
166
|
time = ::Time.parse(value)
|
156
167
|
time .. time
|
157
|
-
end
|
168
|
+
end
|
158
169
|
rescue ArgumentError
|
159
170
|
raise AttrSearchable::IncompatibleDatatype, "Incompatible datatype for #{value}"
|
160
|
-
end
|
171
|
+
end
|
161
172
|
|
162
173
|
def map(value)
|
163
174
|
parse(value).first
|
@@ -180,8 +191,20 @@ module AttrSearchableGrammar
|
|
180
191
|
|
181
192
|
class Date < Datetime
|
182
193
|
def parse(value)
|
183
|
-
|
184
|
-
|
194
|
+
return value .. value unless value.is_a?(::String)
|
195
|
+
|
196
|
+
if value =~ /^[0-9]{4,}$/
|
197
|
+
::Date.new(value.to_i).beginning_of_year .. ::Date.new(value.to_i).end_of_year
|
198
|
+
elsif value =~ /^([0-9]{4,})(\.|-|\/)([0-9]{1,2})$/
|
199
|
+
::Date.new($1.to_i, $3.to_i, 15).beginning_of_month .. ::Date.new($1.to_i, $3.to_i, 15).end_of_month
|
200
|
+
elsif value =~ /^([0-9]{1,2})(\.|-|\/)([0-9]{4,})$/
|
201
|
+
::Date.new($3.to_i, $1.to_i, 15).beginning_of_month .. ::Date.new($3.to_i, $1.to_i, 15).end_of_month
|
202
|
+
else
|
203
|
+
date = ::Date.parse(value)
|
204
|
+
date .. date
|
205
|
+
end
|
206
|
+
rescue ArgumentError
|
207
|
+
raise AttrSearchable::IncompatibleDatatype, "Incompatible datatype for #{value}"
|
185
208
|
end
|
186
209
|
end
|
187
210
|
|
@@ -193,7 +216,7 @@ module AttrSearchableGrammar
|
|
193
216
|
return false if value.to_s =~ /^(0|false|no)$/i
|
194
217
|
|
195
218
|
raise AttrSearchable::IncompatibleDatatype, "Incompatible datatype for #{value}"
|
196
|
-
end
|
219
|
+
end
|
197
220
|
end
|
198
221
|
end
|
199
222
|
end
|
data/test/date_test.rb
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
|
2
|
+
require File.expand_path("../test_helper", __FILE__)
|
3
|
+
|
4
|
+
class DateTest < AttrSearchable::TestCase
|
5
|
+
def test_mapping
|
6
|
+
product = create(:product, :created_on => Date.parse("2014-05-01"))
|
7
|
+
|
8
|
+
assert_includes Product.search("created_on: 2014"), product
|
9
|
+
assert_includes Product.search("created_on: 2014-05"), product
|
10
|
+
assert_includes Product.search("created_on: 2014-05-01"), product
|
11
|
+
end
|
12
|
+
|
13
|
+
def test_anywhere
|
14
|
+
product = create(:product, :created_on => Date.parse("2014-05-01"))
|
15
|
+
|
16
|
+
assert_includes Product.search("2014-05-01"), product
|
17
|
+
refute_includes Product.search("2014-05-02"), product
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_includes
|
21
|
+
product = create(:product, :created_on => Date.parse("2014-05-01"))
|
22
|
+
|
23
|
+
assert_includes Product.search("created_on: 2014-05-01"), product
|
24
|
+
refute_includes Product.search("created_on: 2014-05-02"), product
|
25
|
+
end
|
26
|
+
|
27
|
+
def test_equals
|
28
|
+
product = create(:product, :created_on => Date.parse("2014-05-01"))
|
29
|
+
|
30
|
+
assert_includes Product.search("created_on = 2014-05-01"), product
|
31
|
+
refute_includes Product.search("created_on = 2014-05-02"), product
|
32
|
+
end
|
33
|
+
|
34
|
+
def test_equals_not
|
35
|
+
product = create(:product, :created_on => Date.parse("2014-05-01"))
|
36
|
+
|
37
|
+
assert_includes Product.search("created_on != 2014-05-02"), product
|
38
|
+
refute_includes Product.search("created_on != 2014-05-01"), product
|
39
|
+
end
|
40
|
+
|
41
|
+
def test_greater
|
42
|
+
product = create(:product, :created_on => Date.parse("2014-05-01"))
|
43
|
+
|
44
|
+
assert_includes Product.search("created_on < 2014-05-02"), product
|
45
|
+
refute_includes Product.search("created_on < 2014-05-01"), product
|
46
|
+
end
|
47
|
+
|
48
|
+
def test_greater_equals
|
49
|
+
product = create(:product, :created_on => Date.parse("2014-05-01"))
|
50
|
+
|
51
|
+
assert_includes Product.search("created_on >= 2014-05-01"), product
|
52
|
+
refute_includes Product.search("created_on >= 2014-05-02"), product
|
53
|
+
end
|
54
|
+
|
55
|
+
def test_less
|
56
|
+
product = create(:product, :created_on => Date.parse("2014-05-01"))
|
57
|
+
|
58
|
+
assert_includes Product.search("created_on < 2014-05-02"), product
|
59
|
+
refute_includes Product.search("created_on < 2014-05-01"), product
|
60
|
+
end
|
61
|
+
|
62
|
+
def test_less_equals
|
63
|
+
product = create(:product, :created_on => Date.parse("2014-05-02"))
|
64
|
+
|
65
|
+
assert_includes Product.search("created_on <= 2014-05-02"), product
|
66
|
+
refute_includes Product.search("created_on <= 2014-05-01"), product
|
67
|
+
end
|
68
|
+
|
69
|
+
def test_incompatible_datatype
|
70
|
+
assert_raises AttrSearchable::IncompatibleDatatype do
|
71
|
+
Product.unsafe_search "created_on: Value"
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
data/test/test_helper.rb
CHANGED
@@ -34,11 +34,12 @@ end
|
|
34
34
|
class Product < ActiveRecord::Base
|
35
35
|
include AttrSearchable
|
36
36
|
|
37
|
-
attr_searchable :title, :description, :brand, :stock, :price, :created_at, :available
|
38
|
-
attr_searchable :comment => ["comments.title", "comments.message"], :user => "users.username"
|
39
|
-
|
37
|
+
attr_searchable :title, :description, :brand, :stock, :price, :created_at, :created_on, :available
|
38
|
+
attr_searchable :comment => ["comments.title", "comments.message"], :user => ["users.username", "users_products.username"]
|
40
39
|
attr_searchable :primary => [:title, :description]
|
41
40
|
|
41
|
+
attr_searchable_alias :users_products => :user
|
42
|
+
|
42
43
|
if DATABASE != "sqlite"
|
43
44
|
attr_searchable_options :title, :type => :fulltext
|
44
45
|
attr_searchable_options :description, :type => :fulltext
|
@@ -51,6 +52,8 @@ class Product < ActiveRecord::Base
|
|
51
52
|
|
52
53
|
has_many :comments
|
53
54
|
has_many :users, :through => :comments
|
55
|
+
|
56
|
+
belongs_to :user
|
54
57
|
end
|
55
58
|
|
56
59
|
FactoryGirl.define do
|
@@ -69,11 +72,13 @@ ActiveRecord::Base.connection.execute "DROP TABLE IF EXISTS comments"
|
|
69
72
|
ActiveRecord::Base.connection.execute "DROP TABLE IF EXISTS users"
|
70
73
|
|
71
74
|
ActiveRecord::Base.connection.create_table :products do |t|
|
75
|
+
t.references :user
|
72
76
|
t.string :title
|
73
77
|
t.text :description
|
74
78
|
t.integer :stock
|
75
79
|
t.float :price
|
76
80
|
t.datetime :created_at
|
81
|
+
t.date :created_on
|
77
82
|
t.boolean :available
|
78
83
|
t.string :brand
|
79
84
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: attr_searchable
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.4
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2014-
|
12
|
+
date: 2014-07-05 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: treetop
|
@@ -154,6 +154,7 @@ files:
|
|
154
154
|
- test/attr_searchable_test.rb
|
155
155
|
- test/boolean_test.rb
|
156
156
|
- test/database.yml
|
157
|
+
- test/date_test.rb
|
157
158
|
- test/datetime_test.rb
|
158
159
|
- test/error_test.rb
|
159
160
|
- test/float_test.rb
|
@@ -195,6 +196,7 @@ test_files:
|
|
195
196
|
- test/attr_searchable_test.rb
|
196
197
|
- test/boolean_test.rb
|
197
198
|
- test/database.yml
|
199
|
+
- test/date_test.rb
|
198
200
|
- test/datetime_test.rb
|
199
201
|
- test/error_test.rb
|
200
202
|
- test/float_test.rb
|