bloodhound 0.2.1 → 0.3

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,10 +1,10 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = "bloodhound"
3
- s.version = "0.2.1"
4
- s.date = "2010-02-04"
3
+ s.version = "0.3"
4
+ s.date = "2010-02-07"
5
5
 
6
- s.description = "Convert strings like 'user:foca age:23' to { 'user' => 'foca' => 'age' => 23 }"
7
- s.summary = "Convert strings like 'user:foca age:23' to { 'user' => 'foca' => 'age' => 23 }"
6
+ s.description = "Map strings like 'user:foca age:23' to ActiveRecord named_scopes"
7
+ s.summary = "Map strings like 'user:foca age:23' to ActiveRecord named_scopes"
8
8
  s.homepage = "http://github.com/foca/bloodhound"
9
9
 
10
10
  s.authors = ["Nicolás Sanguinetti"]
@@ -1,82 +1,65 @@
1
1
  require "chronic"
2
+ require "active_record"
2
3
 
3
4
  class Bloodhound
4
- VERSION = "0.2.1"
5
+ VERSION = "0.3"
5
6
  ATTRIBUTE_RE = /\s*(?:(?:(\S+):)?(?:"([^"]*)"|'([^']*)'|(\S+)))\s*/.freeze
6
7
 
7
- def initialize
8
- @search_fields = {}
9
- @keywords = {}
10
- end
8
+ attr_reader :fields
11
9
 
12
- def fields
13
- @search_fields
10
+ def initialize(model)
11
+ @model = model
12
+ @fields = {}
14
13
  end
15
14
 
16
15
  def field(name, options={}, &mapping)
17
- attribute = options.delete(:attribute) || name
18
- fuzzy = options.delete(:fuzzy)
19
- type = options.delete(:type).try(:to_sym)
20
-
21
- fields[name.to_sym] = {
22
- :attribute => attribute.to_s,
23
- :type => type,
24
- :fuzzy => fuzzy.nil? ? true : fuzzy,
25
- :options => options,
16
+ @fields[name.to_sym] = field = {
17
+ :attribute => options.fetch(:attribute, name).to_s,
18
+ :type => options.fetch(:type, :string).to_sym,
19
+ :options => options.except(:attribute, :type),
26
20
  :mapping => mapping || default_mapping
27
21
  }
28
- end
29
22
 
30
- def keyword(name, &block)
31
- @keywords[name.to_sym] = block
23
+ define_scope(name) do |value|
24
+ value = field[:mapping].call(cast_value(value, field[:type]))
25
+ field[:options].merge(:conditions => { field[:attribute] => value })
26
+ end
32
27
  end
33
28
 
34
- def search_scope(model, query)
35
- tokenize(query).inject(model) do |model, (key,value)|
36
- if value.nil?
37
- text_search_for(model, key)
38
- elsif has_keyword?(key)
39
- keyword_search_for(model, key, value)
40
- else
41
- attribute_search_for(model, key, value)
42
- end
29
+ def text_search(*fields)
30
+ options = fields.extract_options!
31
+ fields = fields.map {|f| "LOWER(#{f}) LIKE :value" }.join(" OR ")
32
+
33
+ define_scope "text_search" do |value|
34
+ options.merge(:conditions => [fields, { :value => "%#{value.downcase}%" }])
43
35
  end
44
36
  end
45
37
 
46
- def text_search_for(model, value)
47
- return model if fields.empty?
38
+ def keyword(name, &block)
39
+ define_scope(name, &block)
40
+ end
48
41
 
49
- model = scoped_by_text_search_options(model)
42
+ def search(query)
43
+ self.class.tokenize(query).inject(@model) do |model, (key,value)|
44
+ key, value = "text_search", key if value.nil?
50
45
 
51
- text_search_conditions = fields.inject([[], {}]) do |conditions, (name,properties)|
52
- if properties[:fuzzy]
53
- conditions[0] << "#{properties[:attribute]} LIKE :#{name}"
54
- conditions[1].update(name => properties[:mapping].call("%#{value}%"))
55
- conditions
46
+ if model.respond_to?(scope_name_for(key))
47
+ model.send(scope_name_for(key), value)
56
48
  else
57
- conditions
49
+ model
58
50
  end
59
51
  end
60
-
61
- model = model.scoped(:conditions => [text_search_conditions[0].join(" OR "),
62
- text_search_conditions[1]])
63
52
  end
64
- private :text_search_for
65
-
66
- def attribute_search_for(model, name, value)
67
- properties = fields.fetch(name.to_sym, {})
68
53
 
69
- return model if properties[:type].nil?
70
-
71
- value = properties[:mapping].call(cast_value(value, properties[:type]))
72
- model.scoped(properties[:options].merge(:conditions => { properties[:attribute] => value }))
54
+ def define_scope(name, &scope)
55
+ @model.send(:named_scope, scope_name_for(name), scope)
73
56
  end
74
- private :attribute_search_for
57
+ private :define_scope
75
58
 
76
- def keyword_search_for(model, keyword, value)
77
- model.scoped(@keywords[keyword.to_sym].call(value))
59
+ def scope_name_for(key)
60
+ "bloodhound_for_#{key}"
78
61
  end
79
- private :keyword_search_for
62
+ private :scope_name_for
80
63
 
81
64
  def default_mapping
82
65
  lambda {|value| value }
@@ -108,33 +91,21 @@ class Bloodhound
108
91
  end
109
92
  private :cast_value
110
93
 
111
- def scoped_by_text_search_options(model)
112
- fields.inject(model) do |model, (name,properties)|
113
- properties[:fuzzy] ? model.scoped(properties[:options]) : model
114
- end
115
- end
116
- private :scoped_by_text_search_options
117
-
118
- def tokenize(query)
94
+ def self.tokenize(query)
119
95
  query.scan(ATTRIBUTE_RE).map do |(key,*value)|
120
96
  value = value.compact.first
121
97
  [key || value, key && value]
122
98
  end
123
99
  end
124
- private :tokenize
125
-
126
- def has_keyword?(name)
127
- @keywords.has_key?(name.to_sym)
128
- end
129
100
 
130
101
  module Searchable
131
102
  def bloodhound(&block)
132
- @bloodhound ||= Bloodhound.new
103
+ @bloodhound ||= Bloodhound.new(self)
133
104
  block ? @bloodhound.tap(&block) : @bloodhound
134
105
  end
135
106
 
136
- def scoped_search(query)
137
- bloodhound.search_scope(self, query)
107
+ def scopes_for_query(query)
108
+ bloodhound.search(query)
138
109
  end
139
110
  end
140
111
  end
@@ -6,14 +6,13 @@ class User < ActiveRecord::Base
6
6
  has_many :products
7
7
 
8
8
  bloodhound do |search|
9
- search.field(:first_name, :attribute => "lower(users.first_name)") {|value| value.downcase }
9
+ search.field :first_name
10
10
  search.field :last_name
11
- search.field :product, :attribute => "products.name", :joins => :products, :group => column_names.join(",")
12
11
  search.field :available_product, :type => :boolean,
13
12
  :attribute => "products.available",
14
- :fuzzy => false,
15
- :joins => :products,
16
- :group => column_names.join(",")
13
+ :include => :products
14
+
15
+ search.text_search "first_name", "last_name", "products.name", :include => :products
17
16
  end
18
17
  end
19
18
 
@@ -23,9 +22,11 @@ class Product < ActiveRecord::Base
23
22
  belongs_to :user
24
23
 
25
24
  bloodhound do |search|
25
+ search.text_search "products.name", "products.value"
26
+
26
27
  search.field :name
27
28
  search.field :value
28
- search.field :available, :type => :boolean, :fuzzy => false
29
+ search.field :available, :type => :boolean
29
30
 
30
31
  search.keyword :sort do |value|
31
32
  { :order => "#{search.fields[value.to_sym][:attribute]} DESC" }
@@ -36,51 +37,29 @@ end
36
37
  describe Bloodhound do
37
38
  before :all do
38
39
  @john = User.create(:first_name => "John", :last_name => "Doe")
39
- @tv = @john.products.create(:name => "Rubber Duck", :value => 10)
40
- @ducky = @john.products.create(:name => "TV", :value => 200)
40
+ @ducky = @john.products.create(:name => "Rubber Duck", :value => 10)
41
+ @tv = @john.products.create(:name => "TV", :value => 200)
41
42
  end
42
43
 
43
- it "finds a user by first_name, in a case insensitive manner" do
44
- User.scoped_search("John").should include(@john)
45
- User.scoped_search("joHN").should include(@john)
44
+ it "finds a user by first_name" do
45
+ User.scopes_for_query("first_name:John").should include(@john)
46
46
  end
47
47
 
48
- it "uses LIKE '%query%' for fuzzy searches" do
49
- User.scoped_search("oh").should include(@john)
48
+ it "finds by text search" do
49
+ Product.scopes_for_query("duck rubber").should include(@ducky)
50
50
  end
51
51
 
52
52
  it "finds a user by last_name, but only in a case sensitive manner" do
53
- User.scoped_search("Doe").should include(@john)
54
- User.scoped_search("Doe").should have(1).element
55
- User.scoped_search("Smith").should_not include(@john)
53
+ User.scopes_for_query("last_name:Doe").should include(@john)
54
+ User.scopes_for_query("last_name:Doe").should have(1).element
56
55
  end
57
56
 
58
57
  it "can find using key:value pairs for attribuets that define a type" do
59
- User.scoped_search("available_product:yes").should include(@john)
60
- User.scoped_search("available_product:no").should_not include(@john)
58
+ User.scopes_for_query("available_product:yes").should include(@john)
59
+ User.scopes_for_query("available_product:no").should_not include(@john)
61
60
  end
62
61
 
63
62
  it "allows defining arbitrary keywords to create scopes" do
64
- @john.products.scoped_search("order:name").all.should == [@tv, @ducky]
65
- end
66
-
67
- it "exposes the fields via bloodhound#attributes" do
68
- last_name = User.bloodhound.fields[:last_name]
69
- last_name[:attribute].should == "last_name"
70
- last_name[:type].should be_nil
71
- last_name[:fuzzy].should be_true
72
- last_name[:options].should == {}
73
-
74
- first_name = User.bloodhound.fields[:first_name]
75
- first_name[:attribute].should == "lower(users.first_name)"
76
- first_name[:type].should be_nil
77
- first_name[:fuzzy].should be_true
78
- first_name[:options].should == {}
79
-
80
- availability = Product.bloodhound.fields[:available]
81
- availability[:attribute].should == "available"
82
- availability[:type].should == :boolean
83
- availability[:fuzzy].should be_false
84
- availability[:options].should == {}
63
+ @john.products.scopes_for_query("sort:name").all.should == [@tv, @ducky]
85
64
  end
86
65
  end
@@ -27,18 +27,3 @@ ActiveRecord::Schema.define(:version => 1) do
27
27
  t.belongs_to :user
28
28
  end
29
29
  end
30
-
31
- Spec::Runner.configure do |config|
32
- module CustomMatchers
33
- def be_included_in_extracted_attributes_from(attribute_string)
34
- simple_matcher :other_set_of_attributes do |given|
35
- expected = subject.tokenize(attribute_string)
36
- given.each do |key, value|
37
- expected.fetch(key).should == value
38
- end
39
- end
40
- end
41
- end
42
-
43
- config.include CustomMatchers
44
- end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bloodhound
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: "0.3"
5
5
  platform: ruby
6
6
  authors:
7
7
  - "Nicol\xC3\xA1s Sanguinetti"
@@ -9,11 +9,11 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2010-02-04 00:00:00 -02:00
12
+ date: 2010-02-07 00:00:00 -02:00
13
13
  default_executable:
14
14
  dependencies: []
15
15
 
16
- description: Convert strings like 'user:foca age:23' to { 'user' => 'foca' => 'age' => 23 }
16
+ description: Map strings like 'user:foca age:23' to ActiveRecord named_scopes
17
17
  email: contacto@nicolassanguinetti.info
18
18
  executables: []
19
19
 
@@ -55,6 +55,6 @@ rubyforge_project:
55
55
  rubygems_version: 1.3.5
56
56
  signing_key:
57
57
  specification_version: 3
58
- summary: Convert strings like 'user:foca age:23' to { 'user' => 'foca' => 'age' => 23 }
58
+ summary: Map strings like 'user:foca age:23' to ActiveRecord named_scopes
59
59
  test_files: []
60
60