bloodhound 0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1 @@
1
+ dist
@@ -0,0 +1,118 @@
1
+ Bloodhound
2
+ ==========
3
+
4
+ Simple key:value string conversion to hashes, with type casting.
5
+
6
+ require "bloodhound"
7
+
8
+ hound = Bloodhound.new
9
+ hound.add_search_field(:name, :string)
10
+ hound.add_search_field(:age, :integer)
11
+ hound.add_search_field(:active, :boolean)
12
+
13
+ attributes = hound.attributes_from('name:"John Doe" age:22 active:yes')
14
+ attributes #=> { "name" => "John Doe", "age" => 22, "active" => true }
15
+
16
+ The available types are `:string` (the default), `:integer`, `:float`, `:date`,
17
+ `:time`, and `:boolean`. Any other type is treated as a string.
18
+
19
+ It matches several values as boolean values. 'yes', 'y', and 'true' are all
20
+ mapped to `true`, while 'no', 'n', and 'false' are mapped to `false`.
21
+
22
+ You can customize the hash key returned:
23
+
24
+ hound.add_search_field(:user, :string, "users.name")
25
+
26
+ attributes = hound.attributes_from('user:"John Doe"')
27
+ attributes #=> { "users.name" => "John Doe" }
28
+
29
+ It can match dates and times, using [Chronic](http://github.com/mojombo/chronic),
30
+ so this is valid:
31
+
32
+ hound.add_search_field(:added, :date, "added_on")
33
+
34
+ attributes = hound.attributes_from("added:today")
35
+ attributes #=> { "added_on" => Date.today }
36
+
37
+ Finally, you can also provide processing rules for the parsed value, by passing
38
+ a block:
39
+
40
+ hound.add_search_field(:inactive, :boolean, "active") {|value| not value }
41
+
42
+ attributes = hound.attributes_from("inactive:true")
43
+ attributes #=> { "active" => false }
44
+
45
+ ActiveRecord integration
46
+ ------------------------
47
+
48
+ require "bloodhound/active_record"
49
+
50
+ class Video < ActiveRecord::Base
51
+ extend Bloodhound::Searchable
52
+ named_scope :search, lambda {|query| bloodhound.attributes_from(query) }
53
+ end
54
+
55
+ The ActiveRecord implementation will automatically define search fields for all
56
+ non-id, non-timestamp columns (ie, all except for `id` and `foo_id`).
57
+
58
+ You can, of course, add those in manually if you need them.
59
+
60
+ The syntax for defining attributes is a bit more rails-esque:
61
+
62
+ class Video < ActiveRecord::Base
63
+ extend Bloodhound::Searchable
64
+ search_field :user, :type => :string, :attribute => "users.login"
65
+ end
66
+
67
+ The return value of `ActiveRecord::Bloodhound#attributes_from` changes a bit,
68
+ and returns a hash directly compatible with ActiveRecord:
69
+
70
+ attributes = Video.bloodhound.attributes_from("user:foca")
71
+ attributes #=> { :conditions => { "users.login" => "foca" } }
72
+
73
+ Any extra options you pass to `search_field` are added into the finder options:
74
+
75
+ class Video < ActiveRecord::Base
76
+ extend Bloodhound::Searchable
77
+ search_field :user, :attribute => "users.login", :joins => :user
78
+
79
+ belongs_to :user
80
+ end
81
+
82
+ attributes = Video.bloodhound.attributes_from("user:foca")
83
+ attributes #=> { :joins => :user,
84
+ :conditions => { "users.login" => "foca" } }
85
+
86
+ Known problems
87
+ --------------
88
+
89
+ * Chronic is a bit… weird matching some stuff, specially regarding time zones.
90
+ * The ActiveRecord 'extra options' won't merge. So if you define two search
91
+ fields with ':joins => :an_association', only the latter will remain. This
92
+ will be fixed in a future release.
93
+
94
+ License
95
+ -------
96
+
97
+ (The MIT License)
98
+
99
+ Copyright (c) 2010 Nicolas Sanguinetti, http://nicolassanguinetti.info
100
+
101
+ Permission is hereby granted, free of charge, to any person obtaining
102
+ a copy of this software and associated documentation files (the
103
+ 'Software'), to deal in the Software without restriction, including
104
+ without limitation the rights to use, copy, modify, merge, publish,
105
+ distribute, sublicense, and/or sell copies of the Software, and to
106
+ permit persons to whom the Software is furnished to do so, subject to
107
+ the following conditions:
108
+
109
+ The above copyright notice and this permission notice shall be
110
+ included in all copies or substantial portions of the Software.
111
+
112
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
113
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
114
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
115
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
116
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
117
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
118
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,27 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = "bloodhound"
3
+ s.version = "0.1"
4
+ s.date = "2010-01-15"
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 }"
8
+ s.homepage = "http://github.com/foca/bloodhound"
9
+
10
+ s.authors = ["Nicolás Sanguinetti"]
11
+ s.email = "contacto@nicolassanguinetti.info"
12
+
13
+ s.require_paths = ["lib"]
14
+ s.has_rdoc = false
15
+
16
+ s.files = %w[
17
+ .gitignore
18
+ README.markdown
19
+ bloodhound.gemspec
20
+ lib/bloodhound.rb
21
+ lib/bloodhound/active_record.rb
22
+ spec/spec_helper.rb
23
+ spec/bloodhound_spec.rb
24
+ spec/bloodhound/active_record_spec.rb
25
+ ]
26
+ end
27
+
@@ -0,0 +1,63 @@
1
+ require "chronic"
2
+
3
+ class Bloodhound
4
+ VERSION = "0.1"
5
+ ATTRIBUTE_RE = /\s*(\S+):(?:"([^"]*)"|'([^']*)'|(\S+))\s*/.freeze
6
+
7
+ def initialize
8
+ @mappings = {}
9
+ end
10
+
11
+ def add_search_field(name, type=:string, attribute=name, &mapping)
12
+ @mappings[name.to_sym] = [attribute, type.to_sym, mapping]
13
+ end
14
+
15
+ def attributes_from(query)
16
+ parse_query(query).inject({}) do |conditions, (key,value)|
17
+ attribute, type, mapping = @mappings.fetch(key.to_sym, [])
18
+
19
+ if attribute && type
20
+ conditions[attribute.to_s] = (mapping || default_mapping).call(cast_value(value, type))
21
+ end
22
+
23
+ conditions
24
+ end
25
+ end
26
+
27
+ def default_mapping
28
+ lambda {|value| value }
29
+ end
30
+ private :default_mapping
31
+
32
+ def cast_value(value, type)
33
+ case type
34
+ when :boolean
35
+ { "true" => true,
36
+ "yes" => true,
37
+ "y" => true,
38
+ "false" => false,
39
+ "no" => false,
40
+ "n" => false }.fetch(value.downcase, true)
41
+ when :integer
42
+ # Kernel#Float is a bit more lax about parsing numbers, like
43
+ # Integer(0.0) fails, when we just want it interpreted as a zero
44
+ Float(value).to_i
45
+ when :float, :decimal
46
+ Float(value)
47
+ when :date
48
+ Date.parse(Chronic.parse(value).to_s)
49
+ when :time, :datetime
50
+ Chronic.parse(value)
51
+ else
52
+ value
53
+ end
54
+ end
55
+ private :cast_value
56
+
57
+ def parse_query(query)
58
+ query.scan(ATTRIBUTE_RE).map do |(key,*value)|
59
+ [key, value.compact.first]
60
+ end
61
+ end
62
+ private :parse_query
63
+ end
@@ -0,0 +1,43 @@
1
+ require "bloodhound"
2
+
3
+ module ActiveRecord
4
+ class Bloodhound < ::Bloodhound
5
+ def initialize
6
+ super()
7
+ @extra_options = {}
8
+ end
9
+
10
+ def add_search_field(name, type=:string, attribute=name, options={}, &mapping)
11
+ super(name, type, attribute, &mapping)
12
+ @extra_options[attribute.to_s] = options
13
+ end
14
+
15
+ def attributes_from(query)
16
+ super(query).inject({}) do |finder_options, (name, value)|
17
+ finder_options[:conditions] ||= {}
18
+ finder_options[:conditions].update(name => value)
19
+ finder_options.update(@extra_options.fetch(name, {}))
20
+ finder_options
21
+ end
22
+ end
23
+ end
24
+ end
25
+
26
+ module Bloodhound::Searchable
27
+ def bloodhound
28
+ @bloodhound ||= ActiveRecord::Bloodhound.new
29
+ end
30
+
31
+ def search_field(name, options={}, &mapping)
32
+ attribute = options.delete(:attribute) || name
33
+ type = options.delete(:type) || :string
34
+ bloodhound.add_search_field(name, type, attribute, options, &mapping)
35
+ end
36
+
37
+ def self.extended(model)
38
+ model.columns.each do |column|
39
+ next if column.name.to_s =~ /_?id$/
40
+ model.search_field column.name, :type => column.type
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,44 @@
1
+ require "spec_helper"
2
+ require "bloodhound/active_record"
3
+
4
+ describe ActiveRecord::Bloodhound do
5
+ it "returns a hash with { :conditions => {keys matched} } instead of just the keys" do
6
+ subject.add_search_field(:foo)
7
+ subject.attributes_from("foo:bar").should == { :conditions => { "foo" => "bar" } }
8
+ end
9
+
10
+ it "allows for extra options, that get merged into the return hash" do
11
+ subject.add_search_field(:user, :string, "users.name", :joins => :users)
12
+ subject.attributes_from("user:'John Doe'").should == {
13
+ :joins => :users,
14
+ :conditions => { "users.name" => "John Doe" }
15
+ }
16
+ end
17
+ end
18
+
19
+ describe Bloodhound::Searchable do
20
+ Column = Struct.new(:name, :type)
21
+
22
+ class MockModel
23
+ def self.columns
24
+ [Column.new(:id, :integer), Column.new(:name, :string), Column.new(:user_id, :integer)]
25
+ end
26
+
27
+ extend Bloodhound::Searchable
28
+ end
29
+
30
+ it "allows accessing the bloodhound object in the model by calling Model.bloodhound" do
31
+ MockModel.bloodhound.should be_an(ActiveRecord::Bloodhound)
32
+ end
33
+
34
+ it "adds search fields for the non-id fields in the model" do
35
+ conditions = MockModel.bloodhound.attributes_from("name:John id:3 user_id:5").fetch(:conditions)
36
+ conditions.should_not have_key("id")
37
+ conditions.should_not have_key("user_id")
38
+ end
39
+
40
+ it "allows you to add search fields with a more ActiveRecord-ish syntax" do
41
+ MockModel.search_field :user, :type => :string, :attribute => "users.name"
42
+ MockModel.bloodhound.attributes_from("user:John").should == { :conditions => { "users.name" => "John" } }
43
+ end
44
+ end
@@ -0,0 +1,107 @@
1
+ require "spec_helper"
2
+
3
+ describe Bloodhound do
4
+ context "adding fields" do
5
+ it "can specify a type of :string" do
6
+ subject.add_search_field(:some_string, :string)
7
+ { "some_string" => "falafel" }.should be_included_in_extracted_attributes_from("some_string:falafel")
8
+ end
9
+
10
+ it "can specify a type of :integer" do
11
+ subject.add_search_field(:some_number, :integer)
12
+ { "some_number" => 1 }.should be_included_in_extracted_attributes_from("some_number:1")
13
+ end
14
+
15
+ it "can specify a type of :float" do
16
+ subject.add_search_field(:some_number, :float)
17
+ { "some_number" => 1.5 }.should be_included_in_extracted_attributes_from("some_number:1.5")
18
+ end
19
+
20
+ it "can specify a type of :boolean" do
21
+ subject.add_search_field(:some_boolean, :boolean)
22
+ { "some_boolean" => true }.should be_included_in_extracted_attributes_from("some_boolean:true")
23
+ end
24
+
25
+ it "can specify a type of :date" do
26
+ subject.add_search_field(:some_date, :date)
27
+ { "some_date" => Date.parse('2010-01-05') }.should be_included_in_extracted_attributes_from("some_date:2010-01-05")
28
+ end
29
+
30
+ it "can specifiy a type of :time" do
31
+ subject.add_search_field(:some_time, :time)
32
+ { "some_time" => Time.mktime(2010, 1, 5, 12, 0, 0) }.should be_included_in_extracted_attributes_from("some_time:'2010-01-05 12:00:00'")
33
+ end
34
+
35
+ it "can specify a type of :text" do
36
+ subject.add_search_field(:some_text, :text)
37
+ { "some_text" => "Hello world" }.should be_included_in_extracted_attributes_from("some_text:'Hello world'")
38
+ end
39
+
40
+ it "defaults to a :string field" do
41
+ subject.add_search_field(:some_string)
42
+ { "some_string" => "falafel" }.should be_included_in_extracted_attributes_from("some_string:falafel")
43
+ end
44
+ end
45
+
46
+ context "matching quoted values" do
47
+ before { subject.add_search_field(:foo) }
48
+
49
+ it "matches unquoted values" do
50
+ { "foo" => "bar" }.should be_included_in_extracted_attributes_from("foo:bar")
51
+ { "foo" => "b'a'r" }.should be_included_in_extracted_attributes_from("foo:b'a'r")
52
+ { "foo" => "ba'r" }.should be_included_in_extracted_attributes_from("foo:ba'r")
53
+ end
54
+
55
+ it "matches values within double quotes" do
56
+ { "foo" => "this is awesome" }.should be_included_in_extracted_attributes_from(%Q(foo:"this is awesome"))
57
+ { "foo" => "this is 'tricky'" }.should be_included_in_extracted_attributes_from(%Q(foo:"this is 'tricky'"))
58
+ { "foo" => "'this is even trickier'" }.should be_included_in_extracted_attributes_from(%Q(foo:"'this is even trickier'"))
59
+ end
60
+
61
+ it "matches values within single quotes" do
62
+ { "foo" => 'this is awesome' }.should be_included_in_extracted_attributes_from(%Q(foo:'this is awesome'))
63
+ { "foo" => 'this is "tricky"' }.should be_included_in_extracted_attributes_from(%Q(foo:'this is "tricky"'))
64
+ { "foo" => '"this is even trickier"' }.should be_included_in_extracted_attributes_from(%Q(foo:'"this is even trickier"'))
65
+ end
66
+ end
67
+
68
+ context "casting values" do
69
+ it "matches 'yes', 'true', 'y', 'no', 'false', and 'n' as boolean values" do
70
+ subject.add_search_field(:foo, :boolean)
71
+ { "foo" => true }.should be_included_in_extracted_attributes_from("foo:yes")
72
+ { "foo" => true }.should be_included_in_extracted_attributes_from("foo:y")
73
+ { "foo" => true }.should be_included_in_extracted_attributes_from("foo:true")
74
+ { "foo" => false }.should be_included_in_extracted_attributes_from("foo:no")
75
+ { "foo" => false }.should be_included_in_extracted_attributes_from("foo:n")
76
+ { "foo" => false }.should be_included_in_extracted_attributes_from("foo:false")
77
+ end
78
+
79
+ it "parses integers as Integer objects" do
80
+ subject.add_search_field(:foo, :integer)
81
+ subject.add_search_field(:bar, :integer)
82
+ { "foo" => 1, "bar" => 2 }.should be_included_in_extracted_attributes_from("foo:1 bar:2.8")
83
+ end
84
+
85
+ it "parses decimal values as Float objects" do
86
+ subject.add_search_field(:foo, :float)
87
+ { "foo" => 1.5 }.should be_included_in_extracted_attributes_from("foo:1.5")
88
+ end
89
+
90
+ it "parses dates as instances of Date" do
91
+ subject.add_search_field(:foo, :date)
92
+ { "foo" => Date.today }.should be_included_in_extracted_attributes_from("foo:today")
93
+ end
94
+
95
+ it "parses times as instances of Time" do
96
+ subject.add_search_field(:foo, :time)
97
+ { "foo" => Time.mktime(2010, 1, 5, 12, 0, 0) }.should be_included_in_extracted_attributes_from("foo:2010-01-05 12:00:00")
98
+ end
99
+ end
100
+
101
+ context "processing matched values with user-defined lambdas" do
102
+ it "lets you convert matched values according to your business rules" do
103
+ subject.add_search_field(:incomplete, :boolean, "complete") {|value| not value }
104
+ { "complete" => false }.should be_included_in_extracted_attributes_from("incomplete:yes")
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,18 @@
1
+ require "spec"
2
+ require "ostruct"
3
+ require "bloodhound"
4
+
5
+ Spec::Runner.configure do |config|
6
+ module CustomMatchers
7
+ def be_included_in_extracted_attributes_from(attribute_string)
8
+ simple_matcher :other_set_of_attributes do |given|
9
+ expected = subject.attributes_from(attribute_string)
10
+ given.each do |key, value|
11
+ expected.fetch(key).should == value
12
+ end
13
+ end
14
+ end
15
+ end
16
+
17
+ config.include CustomMatchers
18
+ end
metadata ADDED
@@ -0,0 +1,62 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: bloodhound
3
+ version: !ruby/object:Gem::Version
4
+ version: "0.1"
5
+ platform: ruby
6
+ authors:
7
+ - "Nicol\xC3\xA1s Sanguinetti"
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2010-01-15 00:00:00 -02:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: Convert strings like 'user:foca age:23' to { 'user' => 'foca' => 'age' => 23 }
17
+ email: contacto@nicolassanguinetti.info
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files: []
23
+
24
+ files:
25
+ - .gitignore
26
+ - README.markdown
27
+ - bloodhound.gemspec
28
+ - lib/bloodhound.rb
29
+ - lib/bloodhound/active_record.rb
30
+ - spec/spec_helper.rb
31
+ - spec/bloodhound_spec.rb
32
+ - spec/bloodhound/active_record_spec.rb
33
+ has_rdoc: false
34
+ homepage: http://github.com/foca/bloodhound
35
+ licenses: []
36
+
37
+ post_install_message:
38
+ rdoc_options: []
39
+
40
+ require_paths:
41
+ - lib
42
+ required_ruby_version: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: "0"
47
+ version:
48
+ required_rubygems_version: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: "0"
53
+ version:
54
+ requirements: []
55
+
56
+ rubyforge_project:
57
+ rubygems_version: 1.3.5
58
+ signing_key:
59
+ specification_version: 3
60
+ summary: Convert strings like 'user:foca age:23' to { 'user' => 'foca' => 'age' => 23 }
61
+ test_files: []
62
+