birddog 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,5 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ test.db
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in birddog.gemspec
4
+ gemspec
5
+
6
+ group :development do
7
+ gem "ruby-debug19", :require => 'ruby-debug'
8
+ end
@@ -0,0 +1,12 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rake/testtask'
3
+
4
+ Rake::TestTask.new do |t|
5
+ t.libs.push "lib"
6
+ t.libs.push "specs"
7
+ t.pattern = 'specs/**/*_spec.rb'
8
+ t.verbose = true
9
+ end
10
+
11
+ task :spec => :test
12
+ task :default => :spec
@@ -0,0 +1,67 @@
1
+ = Birddog
2
+ Birddog was birthed from our experience with Bloodhound (github.com/foca/bloodhound) Much of the code in Birddog is derived (lifted) from Bloodhound, but has been extended
3
+ to include aggregates/regexes/testing and is Rails 3 compatible.
4
+
5
+ Birddog and Bloodhound provide functionality to parse "natural language" into scopes for ActiveRecord. They provide the avenue to enable "users" to choose the constraints
6
+ of a scope based on the type of the scope parameters. (Kind of confusing, but super helpful for things like user defined rule based selection)
7
+
8
+ = Using Birddog
9
+ Install it
10
+ gem install birddog # => OR include it in your Gemfile
11
+
12
+ == Example
13
+ Given the following model definitions
14
+
15
+ class User < ActiveRecord::Base
16
+ include Birddog
17
+
18
+ has_many :products
19
+
20
+ birddog do |search|
21
+ search.field :first_name
22
+ search.alias_field :name, :first_name
23
+ search.field :last_name
24
+ search.field :total_product_value, :type => :decimal, :aggregate => "SUM(products.value) AS total_product_value"
25
+ search.field :insensitive_last_name, :attribute => "last_name", :case_sensitive => false
26
+ search.field :substringed_last_name, :attribute => "last_name", :match_substring => true
27
+ search.field :available_product, :type => :boolean,
28
+ :attribute => "products.available",
29
+ :include => :products
30
+
31
+ search.keyword :aggregate_user do
32
+ select(arel_table[:id]).
33
+ joins("LEFT OUTER JOIN products AS products ON (products.user_id = users.id)").
34
+ group(arel_table[:id])
35
+ end
36
+
37
+ search.text_search "first_name", "last_name", "products.name", :include => :products
38
+ end
39
+ end
40
+
41
+ class Product < ActiveRecord::Base
42
+ include Birddog
43
+
44
+ belongs_to :user
45
+
46
+ birddog do |search|
47
+ search.text_search "products.name", "products.value"
48
+
49
+ search.field :name, :regex => true, :wildcard => true
50
+ search.field :value, :type => :decimal
51
+ search.field :available, :type => :boolean
52
+
53
+ search.keyword :sort do |value|
54
+ { :order => "#{search.fields[value.to_sym][:attribute]} DESC" }
55
+ end
56
+ end
57
+ end
58
+
59
+ # The following will return all user ids with an aggregated "total_product_value" greater than 100
60
+ data_set = User.search("total_product_value:>100").search("aggregate_user")
61
+
62
+ You can now use birddog to query the models and run a single query chain to aggregate large datasets
63
+
64
+ There are many more aggregation and selection methods included, but this and the test suite should give you a good start.
65
+
66
+ = License
67
+ Birddog (and subsequently Bloodhound) are licensed under MIT license (Read lib/birddog.rb for full license)
@@ -0,0 +1,28 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "birddog/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "birddog"
7
+ s.version = Birddog::VERSION
8
+ s.authors = ["Brandon Dewitt"]
9
+ s.email = ["brandonsdewitt@gmail.com"]
10
+ s.homepage = ""
11
+ s.summary = %q{Birddog (derived from Bloodhound) is for natural language scope creation and user determined rule selection}
12
+ s.description = %q{Seeeeeeeee Readme}
13
+
14
+ s.rubyforge_project = "birddog"
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
+ s.require_paths = ["lib"]
20
+
21
+ # specify any dependencies here; for example:
22
+ s.add_development_dependency "minitest"
23
+ s.add_development_dependency "rake"
24
+ s.add_development_dependency "sqlite3-ruby"
25
+
26
+ s.add_runtime_dependency "chronic"
27
+ s.add_runtime_dependency "activerecord"
28
+ end
@@ -0,0 +1,180 @@
1
+ # (The MIT License)
2
+ #
3
+ # Copyright (c) 2010 Nicolas Sanguinetti, http://nicolassanguinetti.info
4
+ # Copyright (c) 2012 Brandon Dewitt, http://abrandoned.com
5
+ #
6
+ # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'),
7
+ # to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
8
+ # and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
11
+ #
12
+ # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
13
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
14
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
15
+ # IN THE SOFTWARE.
16
+
17
+ require "chronic"
18
+ require "active_record"
19
+
20
+ require "birddog/version"
21
+ require "birddog/field_conditions"
22
+ require "birddog/boolean_expression"
23
+ require "birddog/date_expression"
24
+ require "birddog/numeric_expression"
25
+ require "birddog/searchable"
26
+
27
+ module Birddog
28
+
29
+ def self.included(base)
30
+ base.extend(::Birddog::Searchable)
31
+ end
32
+
33
+ class Birddog
34
+ include ::Birddog::FieldConditions
35
+
36
+ attr_reader :fields
37
+
38
+ def initialize(model)
39
+ @model = model
40
+ @fields = {}
41
+ end
42
+
43
+ def field(name, options={}, &mapping)
44
+ @fields[name] = field = {
45
+ :attribute => options.fetch(:attribute, name),
46
+ :type => options.fetch(:type, :string),
47
+ :case_sensitive => options.fetch(:case_sensitive, true),
48
+ :match_substring => options.fetch(:match_substring, false),
49
+ :regex => options.fetch(:regex, false),
50
+ :wildcard => options.fetch(:wildcard, false),
51
+ :aggregate => options.fetch(:aggregate, false),
52
+ :options => options.except(:attribute, :type, :case_sensitive, :match_substring, :regex, :wildcard, :aggregate),
53
+ :mapping => mapping || lambda{ |v| v }
54
+ }
55
+
56
+ aggregate?(name) ? define_aggregate_field(name, field) : define_field(name, field)
57
+ end
58
+
59
+ def aggregate?(name)
60
+ @fields[name].fetch(:aggregate, false)
61
+ end
62
+
63
+ def alias_field(name, other_field)
64
+ @fields[name] = @fields[other_field]
65
+ aggregate?(name) ? define_aggregate_field(name, @fields[name]) : define_field(name, @fields[name])
66
+ end
67
+
68
+ def keyword(name, &block)
69
+ define_scope(name, &block)
70
+ end
71
+
72
+ def search(query)
73
+ tokens = tokenize(query)
74
+ tokens.inject(@model) do |model, (key,value)|
75
+ key, value = "text_search", key if value.nil?
76
+ scope_for(model, key, value)
77
+ end
78
+ end
79
+
80
+ def text_search(*fields)
81
+ options = fields.extract_options!
82
+ fields = fields.map { |f| "LOWER(#{f}) LIKE :value" }.join(" OR ")
83
+
84
+ define_scope "text_search" do |value|
85
+ options.merge(:conditions => [fields, { :value => "%#{value.downcase}%" }])
86
+ end
87
+ end
88
+
89
+ ################## PRIVATES ####################
90
+
91
+ def conditional?(value)
92
+ value.index(/[<>=]/) != nil
93
+ end
94
+ private :conditional?
95
+
96
+ def define_aggregate_field(name, field)
97
+ field[:options].merge!(:select => field[:aggregate])
98
+
99
+ define_scope(name) do |value|
100
+ if conditional?(value)
101
+ field[:options].merge(:having => setup_conditions(field, value))
102
+ else
103
+ field[:options]
104
+ end
105
+ end
106
+ end
107
+ private :define_aggregate_field
108
+
109
+ def define_field(name, field)
110
+ define_scope(name) do |value|
111
+ field[:options].merge(:conditions => setup_conditions(field, value))
112
+ end
113
+ end
114
+ private :define_field
115
+
116
+ def scope_name_for(key)
117
+ "_birddog_scope_#{key}"
118
+ end
119
+ private :scope_name_for
120
+
121
+ def define_scope(name, &scope)
122
+ @model.send(:scope, scope_name_for(name), scope)
123
+ end
124
+ private :define_scope
125
+
126
+ def cast_value(value, type)
127
+ case type
128
+ when :boolean then
129
+ BooleanExpression.parse(value)
130
+ when :float, :decimal, :integer then
131
+ NumericExpression.new(value, type)
132
+ when :date then
133
+ DateExpression.new(value)
134
+ when :time, :datetime then
135
+ Chronic.parse(value)
136
+ else
137
+ value.strip
138
+ end
139
+ end
140
+ private :cast_value
141
+
142
+ def setup_conditions(field, value)
143
+ value = cast_value(value, field[:type])
144
+ value = field[:mapping].call(value)
145
+
146
+ case field[:type]
147
+ when :string then
148
+ conditions_for_string_search(field, value)
149
+ when :float, :decimal, :integer then
150
+ conditions_for_numeric(field, value)
151
+ when :date, :datetime, :time then
152
+ conditions_for_date(field, value)
153
+ else
154
+ { field[:attribute] => value }
155
+ end
156
+ end
157
+ private :setup_conditions
158
+
159
+ def tokenize(query)
160
+ split_tokens = query.split(":")
161
+ split_tokens.each { |tok| tok.strip! }
162
+
163
+ [[split_tokens.shift, split_tokens.join(":")]]
164
+ end
165
+ private :tokenize
166
+
167
+ def scope_for(model, key, value)
168
+ scope_name = scope_name_for(key)
169
+
170
+ if model.respond_to?(scope_name)
171
+ model.send(scope_name, value)
172
+ else
173
+ model.scoped
174
+ end
175
+ end
176
+ private :scope_for
177
+
178
+ end
179
+
180
+ end
@@ -0,0 +1,16 @@
1
+ module Birddog
2
+
3
+ class BooleanExpression
4
+
5
+ def self.parse(value)
6
+ { "true" => true,
7
+ "yes" => true,
8
+ "y" => true,
9
+ "false" => false,
10
+ "no" => false,
11
+ "n" => false }.fetch("#{value.downcase}", true)
12
+ end
13
+
14
+ end
15
+
16
+ end
@@ -0,0 +1,24 @@
1
+ module Birddog
2
+
3
+ class DateExpression
4
+ attr_reader :value, :condition
5
+
6
+ def initialize(value)
7
+ # allow additional spaces to be entered between conditions and value
8
+ value.gsub!(/\s/, '')
9
+ parts = value.scan(/(?:[=<>]+|(?:[0-9]{1,2}\/[0-9]{1,2}\/[0-9]{4}))/)[0,2]
10
+ @value = Date.parse(Chronic.parse(parts.last).to_s)
11
+ @condition = sanitize_condition(parts.first)
12
+ end
13
+
14
+ def sanitize_condition(cond)
15
+ valid = %w(= == > < <= >= <>)
16
+ valid.include?(cond) ? cond.strip : "="
17
+ end
18
+
19
+ def to_s
20
+ @value.to_s
21
+ end
22
+ end
23
+
24
+ end
@@ -0,0 +1,50 @@
1
+ module Birddog
2
+
3
+ module FieldConditions
4
+
5
+ def conditions_for_string_search(field, value)
6
+ search_con = " LIKE ? "
7
+ field_to_search = "lower(#{field[:attribute]})"
8
+ value_to_search = value.downcase
9
+
10
+ if field[:case_sensitive]
11
+ field_to_search = field[:attribute]
12
+ value_to_search = value
13
+ end
14
+
15
+ if field[:match_substring]
16
+ value_to_search = "%#{value_to_search}%"
17
+ end
18
+
19
+ if field[:regex] && regexed?(value)
20
+ # TODO check db driver to determine regex operator for DB (current is Postgres)
21
+ search_con = " ~ ? "
22
+ value_to_search = value[1..value.size-2]
23
+ field_to_search = field[:attribute]
24
+ elsif field[:wildcard] && value_to_search.include?("*")
25
+ value_to_search.gsub!(/[\*]/, "%")
26
+ end
27
+
28
+ [ "#{field_to_search} #{search_con} ", value_to_search ]
29
+ end
30
+
31
+ def conditions_for_date(field, value)
32
+ [ "#{field[:attribute]} #{value.condition} ? ", value.value.strftime("%Y-%m-%d")]
33
+ end
34
+
35
+ def conditions_for_numeric(field, value)
36
+ case value.condition
37
+ when "=" then
38
+ [ "ABS(#{field[:attribute]}) >= ? AND ABS(#{field[:attribute]}) < ?", value.to_f.abs.floor, (value.to_f.abs + 1).floor ]
39
+ else
40
+ [ "#{field[:attribute]} #{value.condition} ? ", value.to_f ]
41
+ end
42
+ end
43
+
44
+ def regexed?(value)
45
+ (value[0].chr == '/' && value[-1].chr == '/')
46
+ end
47
+
48
+ end
49
+
50
+ end
@@ -0,0 +1,35 @@
1
+ module Birddog
2
+
3
+ class NumericExpression
4
+
5
+ attr_reader :condition
6
+
7
+ def initialize(value, type)
8
+ # allow additional spaces to be entered between conditions and value
9
+ value.gsub!(/\s/, '')
10
+ parts = value.scan(/(?:[=<>]+|(?:-?\d|\.)+)/)[0,2]
11
+
12
+ @value = Float(parts.last)
13
+ @value = @value.to_i if type == :integer
14
+ @condition = sanitize_condition(parts.first)
15
+ end
16
+
17
+ def sanitize_condition(cond)
18
+ valid = %w(= == > < <= >= <>)
19
+ valid.include?(cond) ? cond.strip : "="
20
+ end
21
+
22
+ def to_i
23
+ @value.to_i
24
+ end
25
+
26
+ def to_f
27
+ @value.to_f
28
+ end
29
+
30
+ def to_s
31
+ @value.to_s
32
+ end
33
+ end
34
+
35
+ end
@@ -0,0 +1,16 @@
1
+ module Birddog
2
+
3
+ module Searchable
4
+
5
+ def birddog(&block)
6
+ @birddog ||= ::Birddog::Birddog.new(self)
7
+ block ? @birddog.tap(&block) : @birddog
8
+ end
9
+
10
+ def scopes_for_query(query)
11
+ birddog.search(query)
12
+ end
13
+
14
+ end
15
+
16
+ end
@@ -0,0 +1,3 @@
1
+ module Birddog
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,101 @@
1
+ require "spec_helper"
2
+
3
+ describe Birddog::Birddog do ####################
4
+ before :each do
5
+ User.destroy_all
6
+ Product.destroy_all
7
+ @john = User.create(:first_name => "John", :last_name => "Doe")
8
+ @ducky = @john.products.create(:name => "Rubber Duck", :value => 10)
9
+ @tv = @john.products.create(:name => "TV", :value => 200)
10
+ end
11
+
12
+ it "finds a user by first_name" do
13
+ User.scopes_for_query("first_name:John").must_include(@john)
14
+ end
15
+
16
+ it "finds a user by first_name, using an alias" do
17
+ User.scopes_for_query("name:John").must_include(@john)
18
+ end
19
+
20
+ it "finds by text search" do
21
+ Product.scopes_for_query("duck rubber").must_include(@ducky)
22
+ end
23
+
24
+ # TODO figure out the cross platform way
25
+ # it "finds by regex search" do
26
+ # Product.scopes_for_query("name : /TV/").must_include(@tv)
27
+ # end
28
+
29
+ it "finds by wildcard character" do
30
+ Product.scopes_for_query("name : T*").must_include(@tv)
31
+ end
32
+
33
+ it "finds a user by last_name, but only in a case sensitive manner" do
34
+ User.scopes_for_query("last_name:Doe").must_include(@john)
35
+ User.scopes_for_query("last_name:Doe").size.must_equal(1)
36
+ end
37
+
38
+ it "can find using case insensitive search" do
39
+ User.scopes_for_query("insensitive_last_name:doe").must_include(@john)
40
+ User.scopes_for_query("insensitive_last_name:doe").size.must_equal(1)
41
+ end
42
+
43
+ it "can find matching substrings" do
44
+ User.scopes_for_query("substringed_last_name:oe").must_include(@john)
45
+ User.scopes_for_query("substringed_last_name:oe").size.must_equal(1)
46
+ end
47
+
48
+ it "can find using key:value pairs for attributes that define a type" do
49
+ User.scopes_for_query("available_product:yes").must_include(@john)
50
+ User.scopes_for_query("available_product:no").wont_include(@john)
51
+ end
52
+
53
+ it "allows defining arbitrary keywords to create scopes" do
54
+ @john.products.scopes_for_query("sort:name").all.must_equal([@tv, @ducky])
55
+ end
56
+
57
+ describe "numeric fields" do ########################
58
+ it "searches by equals" do
59
+ @john.products.scopes_for_query("value:=10").must_include(@ducky)
60
+ @john.products.scopes_for_query("value:=10").size.must_equal(1)
61
+ end
62
+
63
+ it "searches by equality without =" do
64
+ @john.products.scopes_for_query("value:10").must_include(@ducky)
65
+ @john.products.scopes_for_query("value:10").size.must_equal(1)
66
+ end
67
+
68
+ it "searches by greater than" do
69
+ @john.products.scopes_for_query("value:>100").must_include(@tv)
70
+ @john.products.scopes_for_query("value:>100").size.must_equal(1)
71
+ end
72
+
73
+ it "searches by less than" do
74
+ @john.products.scopes_for_query("value:<100").must_include(@ducky)
75
+ @john.products.scopes_for_query("value:<100").size.must_equal(1)
76
+ end
77
+
78
+ it "searches by =>" do
79
+ @john.products.scopes_for_query("value:>=200").must_include(@tv)
80
+ @john.products.scopes_for_query("value:>=200").size.must_equal(1)
81
+ end
82
+
83
+ it "searches on negatives" do
84
+ @john.products.scopes_for_query("value:>=-200").must_include(@tv)
85
+ @john.products.scopes_for_query("value:>=-200").must_include(@ducky)
86
+ @john.products.scopes_for_query("value:>=-200").size.must_equal(2)
87
+ end
88
+
89
+ describe "spacing" do ###########################
90
+ specify { @john.products.scopes_for_query("value : >= 200").must_include(@tv) }
91
+ specify { @john.products.scopes_for_query("value : >=200").must_include(@tv) }
92
+ specify { @john.products.scopes_for_query("value: >= 200 ").must_include(@tv) }
93
+ specify { @john.products.scopes_for_query("value :>= 200").must_include(@tv) }
94
+ specify { @john.products.scopes_for_query("value:>= 200").must_include(@tv) }
95
+ specify { @john.products.scopes_for_query(" value:>= 200").must_include(@tv) }
96
+ specify { @john.products.scopes_for_query("value: >=200").must_include(@tv) }
97
+ end
98
+
99
+ end
100
+
101
+ end
@@ -0,0 +1,52 @@
1
+ require "spec_helper"
2
+
3
+ describe Birddog::Birddog do
4
+ before :each do
5
+ User.destroy_all
6
+ Product.destroy_all
7
+ @john = User.create(:first_name => "John", :last_name => "Doe")
8
+ @ducky = @john.products.create(:name => "Rubber Duck", :value => 10)
9
+ @tv = @john.products.create(:name => "TV", :value => 200)
10
+ @bike = @john.products.create(:name => "Bike", :value => 100)
11
+ end
12
+
13
+ describe "aggregates" do
14
+ it "doesn't include HAVING clause when no condition included" do
15
+ sql = User.scopes_for_query("aggregate_user").scopes_for_query("total_product_value").to_sql
16
+ sql.wont_match(/HAVING/i)
17
+ end
18
+
19
+ it "includes HAVING clause when condition included" do
20
+ sql = User.scopes_for_query("aggregate_user").scopes_for_query("total_product_value:>0").to_sql
21
+ sql.must_match(/HAVING/i)
22
+ end
23
+
24
+ it "calculates sums" do
25
+ sum = User.scopes_for_query("aggregate_user").
26
+ scopes_for_query("total_product_value:>0")
27
+
28
+ db_sum = User.first.products.inject(0.0) {|a,n| a + n.value }
29
+ sum.first.total_product_value.must_equal(db_sum)
30
+ end
31
+ end
32
+
33
+ describe "chained scopes" do
34
+ it "chains numeric" do
35
+ products = Product.scopes_for_query("value: > 11").
36
+ scopes_for_query("value: < 101")
37
+
38
+ products.size.must_equal(1)
39
+ products.must_include(@bike)
40
+ end
41
+
42
+ it "chains numeric/wildcard" do
43
+ products = Product.scopes_for_query("value: > 11").
44
+ scopes_for_query("value: < 300").
45
+ scopes_for_query("name: T*")
46
+
47
+ products.size.must_equal(1)
48
+ products.must_include(@tv)
49
+ end
50
+ end
51
+
52
+ end
@@ -0,0 +1,25 @@
1
+ require "spec_helper"
2
+
3
+ describe ::Birddog::FieldConditions do ##############
4
+ class RegexTestClass
5
+ extend ::Birddog::FieldConditions
6
+ end
7
+
8
+ ["/regex/",
9
+ "/man on a buffalo//",
10
+ " /straight up mauled/",
11
+ "/by a cougar/ ",
12
+ " /buffalo/ "].each do |reg|
13
+ it "identifies #{reg} as regex" do
14
+ RegexTestClass.regexed?(reg.strip).must_equal(true)
15
+ end
16
+ end
17
+
18
+ ["regex/",
19
+ "/man on a buffalo",
20
+ " /straight up mauled/ and stuff"].each do |reg|
21
+ it "doesnt identify #{reg} as regex" do
22
+ RegexTestClass.regexed?(reg.strip).must_equal(false)
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,75 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ Bundler.require(:default, :development, :test)
4
+
5
+ require 'minitest/spec'
6
+ require 'minitest/autorun'
7
+ require 'minitest/pride'
8
+ require 'support/minitest_matchers'
9
+
10
+ ActiveRecord::Base.establish_connection(
11
+ :adapter => "sqlite3",
12
+ :database => "specs/test.db"
13
+ )
14
+
15
+ ActiveRecord::Base.connection.tables.each do |table|
16
+ ActiveRecord::Base.connection.drop_table(table)
17
+ end
18
+
19
+ ActiveRecord::Schema.define(:version => 1) do
20
+ create_table :users do |t|
21
+ t.string :first_name
22
+ t.string :last_name
23
+ end
24
+
25
+ create_table :products do |t|
26
+ t.string :name
27
+ t.integer :value
28
+ t.boolean :available, :default => true
29
+ t.belongs_to :user
30
+ end
31
+ end
32
+
33
+ class User < ActiveRecord::Base
34
+ include Birddog
35
+
36
+ has_many :products
37
+
38
+ birddog do |search|
39
+ search.field :first_name
40
+ search.alias_field :name, :first_name
41
+ search.field :last_name
42
+ search.field :total_product_value, :type => :decimal, :aggregate => "SUM(products.value) AS total_product_value"
43
+ search.field :insensitive_last_name, :attribute => "last_name", :case_sensitive => false
44
+ search.field :substringed_last_name, :attribute => "last_name", :match_substring => true
45
+ search.field :available_product, :type => :boolean,
46
+ :attribute => "products.available",
47
+ :include => :products
48
+
49
+ search.keyword :aggregate_user do
50
+ select(arel_table[:id]).
51
+ joins("LEFT OUTER JOIN products AS products ON (products.user_id = users.id)").
52
+ group(arel_table[:id])
53
+ end
54
+
55
+ search.text_search "first_name", "last_name", "products.name", :include => :products
56
+ end
57
+ end
58
+
59
+ class Product < ActiveRecord::Base
60
+ include Birddog
61
+
62
+ belongs_to :user
63
+
64
+ birddog do |search|
65
+ search.text_search "products.name", "products.value"
66
+
67
+ search.field :name, :regex => true, :wildcard => true
68
+ search.field :value, :type => :decimal
69
+ search.field :available, :type => :boolean
70
+
71
+ search.keyword :sort do |value|
72
+ { :order => "#{search.fields[value.to_sym][:attribute]} DESC" }
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,5 @@
1
+ require 'minitest/spec'
2
+
3
+ module MiniTest::Expectations
4
+ infect_an_assertion :assert_equal, :must_be
5
+ end
metadata ADDED
@@ -0,0 +1,120 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: birddog
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Brandon Dewitt
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-01-31 00:00:00.000000000Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: minitest
16
+ requirement: &12927140 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: *12927140
25
+ - !ruby/object:Gem::Dependency
26
+ name: rake
27
+ requirement: &12926220 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: *12926220
36
+ - !ruby/object:Gem::Dependency
37
+ name: sqlite3-ruby
38
+ requirement: &12867540 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ type: :development
45
+ prerelease: false
46
+ version_requirements: *12867540
47
+ - !ruby/object:Gem::Dependency
48
+ name: chronic
49
+ requirement: &12866680 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ type: :runtime
56
+ prerelease: false
57
+ version_requirements: *12866680
58
+ - !ruby/object:Gem::Dependency
59
+ name: activerecord
60
+ requirement: &12866120 !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ! '>='
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ type: :runtime
67
+ prerelease: false
68
+ version_requirements: *12866120
69
+ description: Seeeeeeeee Readme
70
+ email:
71
+ - brandonsdewitt@gmail.com
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - .gitignore
77
+ - Gemfile
78
+ - Rakefile
79
+ - Readme.rdoc
80
+ - birddog.gemspec
81
+ - lib/birddog.rb
82
+ - lib/birddog/boolean_expression.rb
83
+ - lib/birddog/date_expression.rb
84
+ - lib/birddog/field_conditions.rb
85
+ - lib/birddog/numeric_expression.rb
86
+ - lib/birddog/searchable.rb
87
+ - lib/birddog/version.rb
88
+ - specs/birddog_spec.rb
89
+ - specs/complex_scope_spec.rb
90
+ - specs/field_conditions_spec.rb
91
+ - specs/spec_helper.rb
92
+ - specs/support/minitest_matchers.rb
93
+ - specs/test.db
94
+ homepage: ''
95
+ licenses: []
96
+ post_install_message:
97
+ rdoc_options: []
98
+ require_paths:
99
+ - lib
100
+ required_ruby_version: !ruby/object:Gem::Requirement
101
+ none: false
102
+ requirements:
103
+ - - ! '>='
104
+ - !ruby/object:Gem::Version
105
+ version: '0'
106
+ required_rubygems_version: !ruby/object:Gem::Requirement
107
+ none: false
108
+ requirements:
109
+ - - ! '>='
110
+ - !ruby/object:Gem::Version
111
+ version: '0'
112
+ requirements: []
113
+ rubyforge_project: birddog
114
+ rubygems_version: 1.8.10
115
+ signing_key:
116
+ specification_version: 3
117
+ summary: Birddog (derived from Bloodhound) is for natural language scope creation
118
+ and user determined rule selection
119
+ test_files: []
120
+ has_rdoc: