birddog 0.0.3 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -2,4 +2,4 @@
2
2
  .bundle
3
3
  Gemfile.lock
4
4
  pkg/*
5
- test.db
5
+ specs/*.db
data/birddog.gemspec CHANGED
@@ -25,4 +25,6 @@ Gem::Specification.new do |s|
25
25
 
26
26
  s.add_runtime_dependency "chronic"
27
27
  s.add_runtime_dependency "activerecord"
28
+ s.add_runtime_dependency "activesupport"
29
+ s.add_runtime_dependency "squeel"
28
30
  end
@@ -0,0 +1,178 @@
1
+ module Birddog
2
+
3
+ def self.included(base)
4
+ base.extend(::Birddog::Searchable)
5
+ end
6
+
7
+ class Birddog
8
+ include ::Birddog::FieldConditions
9
+
10
+ AREL_KEYS = [:select, :limit, :joins, :includes, :group, :from, :where, :having]
11
+
12
+ attr_reader :fields
13
+
14
+ def initialize(model)
15
+ @model = model
16
+ @fields = {}
17
+ end
18
+
19
+ def field(name, options={}, &mapping)
20
+ @fields[name] = field = {
21
+ :attribute => options.fetch(:attribute, name),
22
+ :cast => options.fetch(:cast, false),
23
+ :type => options.fetch(:type, :string),
24
+ :case_sensitive => options.fetch(:case_sensitive, true),
25
+ :match_substring => options.fetch(:match_substring, false),
26
+ :regex => options.fetch(:regex, false),
27
+ :wildcard => options.fetch(:wildcard, false),
28
+ :aggregate => options.fetch(:aggregate, false),
29
+ :options => options.select { |k,v| AREL_KEYS.include?(k) },
30
+ :mapping => mapping || lambda{ |v| v }
31
+ }
32
+
33
+ aggregate?(name) ? define_aggregate_field(name, field) : define_field(name, field)
34
+ end
35
+
36
+ def aggregate?(name)
37
+ @fields[name].fetch(:aggregate, false)
38
+ end
39
+
40
+ def alias_field(name, other_field)
41
+ @fields[name] = @fields[other_field]
42
+ aggregate?(name) ? define_aggregate_field(name, @fields[name]) : define_field(name, @fields[name])
43
+ end
44
+
45
+ def keyword(name, &block)
46
+ define_scope(name, &block)
47
+ end
48
+
49
+ def search(query)
50
+ tokens = tokenize(query)
51
+ tokens.inject(@model) do |model, (key,value)|
52
+ key, value = "text_search", key if value.nil?
53
+ scope_for(model, key, value)
54
+ end
55
+ end
56
+
57
+ def text_search(*fields)
58
+ options = fields.extract_options!
59
+ fields = fields.map { |f| "LOWER(#{f}) LIKE :value" }.join(" OR ")
60
+
61
+ define_scope "text_search" do |value|
62
+ options.merge(:conditions => [fields, { :value => "%#{value.downcase}%" }])
63
+ end
64
+ end
65
+
66
+ ################## PRIVATES ####################
67
+
68
+ def conditional?(value)
69
+ value.index(/[<>=]/) != nil
70
+ end
71
+ private :conditional?
72
+
73
+ def define_aggregate_field(name, field)
74
+ field[:options].merge!(:select => field[:aggregate])
75
+
76
+ define_scope(name) do |value|
77
+ if conditional?(value)
78
+ field[:options].merge(:having => setup_conditions(field, value))
79
+ else
80
+ field[:options]
81
+ end
82
+ end
83
+ end
84
+ private :define_aggregate_field
85
+
86
+ def define_field(name, field)
87
+ define_scope(name) do |value|
88
+ field[:options].merge(:conditions => setup_conditions(field, value))
89
+ end
90
+ end
91
+ private :define_field
92
+
93
+ def scope_name_for(key)
94
+ "_birddog_scope_#{key}"
95
+ end
96
+ private :scope_name_for
97
+
98
+ def define_scope(name, &scope)
99
+ @model.send(:scope, scope_name_for(name), scope)
100
+ end
101
+ private :define_scope
102
+
103
+
104
+ def callable_or_cast(field, condition, value)
105
+ if field[:cast] && field[:cast].respond_to?(:call)
106
+ field[:cast].call(value.gsub(condition, ""))
107
+ else
108
+ cast_value(value, field[:type])
109
+ end
110
+ end
111
+ private :callable_or_cast
112
+
113
+ def cast_value(value, type)
114
+ case type
115
+ when :boolean then
116
+ BooleanExpression.parse(value)
117
+ when :float, :decimal, :integer then
118
+ NumericExpression.new(value, type)
119
+ when :date then
120
+ DateExpression.new(value)
121
+ when :time, :datetime then
122
+ Chronic.parse(value)
123
+ else
124
+ value.strip
125
+ end
126
+ end
127
+ private :cast_value
128
+
129
+ def parse_condition(value)
130
+ valid = %w(= == > < <= >= <>)
131
+ value.gsub!(/\s/, '')
132
+
133
+ parts = value.scan(/(?:[=<>]+)/)
134
+ cond = parts.first
135
+ valid.include?(cond) ? cond.strip : "="
136
+ end
137
+ private :parse_condition
138
+
139
+ def setup_conditions(field, value)
140
+ condition = parse_condition(value)
141
+ value = callable_or_cast(field, condition, value)
142
+ value = field[:mapping].call(value)
143
+
144
+ case field[:type]
145
+ when :string then
146
+ conditions_for_string_search(field, value)
147
+ when :float, :decimal, :integer then
148
+ conditions_for_numeric(field, condition, value)
149
+ when :date, :datetime, :time then
150
+ conditions_for_date(field, condition, value)
151
+ else
152
+ { field[:attribute] => value }
153
+ end
154
+ end
155
+ private :setup_conditions
156
+
157
+ def tokenize(query)
158
+ split_tokens = query.split(":")
159
+ split_tokens.each { |tok| tok.strip! }
160
+
161
+ [[split_tokens.shift, split_tokens.join(":")]]
162
+ end
163
+ private :tokenize
164
+
165
+ def scope_for(model, key, value)
166
+ scope_name = scope_name_for(key)
167
+
168
+ if model.respond_to?(scope_name)
169
+ model.send(scope_name, value)
170
+ else
171
+ model.scoped
172
+ end
173
+ end
174
+ private :scope_for
175
+
176
+ end
177
+
178
+ end
@@ -1,3 +1,3 @@
1
1
  module Birddog
2
- VERSION = "0.0.3"
2
+ VERSION = "0.0.4"
3
3
  end
data/lib/birddog.rb CHANGED
@@ -14,8 +14,14 @@
14
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
15
  # IN THE SOFTWARE.
16
16
 
17
- require "chronic"
18
- require "active_record"
17
+ require 'chronic'
18
+ require 'active_support/core_ext'
19
+ require 'active_record'
20
+ require 'squeel'
21
+
22
+ Squeel.configure do |config|
23
+ config.load_core_extensions :hash
24
+ end
19
25
 
20
26
  require "birddog/version"
21
27
  require "birddog/field_conditions"
@@ -23,180 +29,4 @@ require "birddog/boolean_expression"
23
29
  require "birddog/date_expression"
24
30
  require "birddog/numeric_expression"
25
31
  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
- :cast => options.fetch(:cast, false),
47
- :type => options.fetch(:type, :string),
48
- :case_sensitive => options.fetch(:case_sensitive, true),
49
- :match_substring => options.fetch(:match_substring, false),
50
- :regex => options.fetch(:regex, false),
51
- :wildcard => options.fetch(:wildcard, false),
52
- :aggregate => options.fetch(:aggregate, false),
53
- :options => options.except(:attribute, :type, :case_sensitive, :match_substring, :regex, :wildcard, :aggregate, :cast),
54
- :mapping => mapping || lambda{ |v| v }
55
- }
56
-
57
- aggregate?(name) ? define_aggregate_field(name, field) : define_field(name, field)
58
- end
59
-
60
- def aggregate?(name)
61
- @fields[name].fetch(:aggregate, false)
62
- end
63
-
64
- def alias_field(name, other_field)
65
- @fields[name] = @fields[other_field]
66
- aggregate?(name) ? define_aggregate_field(name, @fields[name]) : define_field(name, @fields[name])
67
- end
68
-
69
- def keyword(name, &block)
70
- define_scope(name, &block)
71
- end
72
-
73
- def search(query)
74
- tokens = tokenize(query)
75
- tokens.inject(@model) do |model, (key,value)|
76
- key, value = "text_search", key if value.nil?
77
- scope_for(model, key, value)
78
- end
79
- end
80
-
81
- def text_search(*fields)
82
- options = fields.extract_options!
83
- fields = fields.map { |f| "LOWER(#{f}) LIKE :value" }.join(" OR ")
84
-
85
- define_scope "text_search" do |value|
86
- options.merge(:conditions => [fields, { :value => "%#{value.downcase}%" }])
87
- end
88
- end
89
-
90
- ################## PRIVATES ####################
91
-
92
- def conditional?(value)
93
- value.index(/[<>=]/) != nil
94
- end
95
- private :conditional?
96
-
97
- def define_aggregate_field(name, field)
98
- field[:options].merge!(:select => field[:aggregate])
99
-
100
- define_scope(name) do |value|
101
- if conditional?(value)
102
- field[:options].merge(:having => setup_conditions(field, value))
103
- else
104
- field[:options]
105
- end
106
- end
107
- end
108
- private :define_aggregate_field
109
-
110
- def define_field(name, field)
111
- define_scope(name) do |value|
112
- field[:options].merge(:conditions => setup_conditions(field, value))
113
- end
114
- end
115
- private :define_field
116
-
117
- def scope_name_for(key)
118
- "_birddog_scope_#{key}"
119
- end
120
- private :scope_name_for
121
-
122
- def define_scope(name, &scope)
123
- @model.send(:scope, scope_name_for(name), scope)
124
- end
125
- private :define_scope
126
-
127
-
128
- def callable_or_cast(field, value)
129
- if field[:cast] && field[:cast].respond_to?(:call)
130
- field[:cast].call(value)
131
- else
132
- cast_value(value, field[:type])
133
- end
134
- end
135
- private :callable_or_cast
136
-
137
- def cast_value(value, type)
138
- case type
139
- when :boolean then
140
- BooleanExpression.parse(value)
141
- when :float, :decimal, :integer then
142
- NumericExpression.new(value, type)
143
- when :date then
144
- DateExpression.new(value)
145
- when :time, :datetime then
146
- Chronic.parse(value)
147
- else
148
- value.strip
149
- end
150
- end
151
- private :cast_value
152
-
153
- def parse_condition(value)
154
- valid = %w(= == > < <= >= <>)
155
- value.gsub!(/\s/, '')
156
-
157
- parts = value.scan(/(?:[=<>]+)/)
158
- cond = parts.first
159
- valid.include?(cond) ? cond.strip : "="
160
- end
161
- private :parse_condition
162
-
163
- def setup_conditions(field, value)
164
- condition = parse_condition(value)
165
- value = callable_or_cast(field, value)
166
- value = field[:mapping].call(value)
167
-
168
- case field[:type]
169
- when :string then
170
- conditions_for_string_search(field, value)
171
- when :float, :decimal, :integer then
172
- conditions_for_numeric(field, condition, value)
173
- when :date, :datetime, :time then
174
- conditions_for_date(field, condition, value)
175
- else
176
- { field[:attribute] => value }
177
- end
178
- end
179
- private :setup_conditions
180
-
181
- def tokenize(query)
182
- split_tokens = query.split(":")
183
- split_tokens.each { |tok| tok.strip! }
184
-
185
- [[split_tokens.shift, split_tokens.join(":")]]
186
- end
187
- private :tokenize
188
-
189
- def scope_for(model, key, value)
190
- scope_name = scope_name_for(key)
191
-
192
- if model.respond_to?(scope_name)
193
- model.send(scope_name, value)
194
- else
195
- model.scoped
196
- end
197
- end
198
- private :scope_for
199
-
200
- end
201
-
202
- end
32
+ require "birddog/birddog"
@@ -13,7 +13,17 @@ describe Birddog::Birddog do
13
13
  describe "castable" do
14
14
  it "casts Date when a cast function is provided" do
15
15
  prods = Product.scopes_for_query("cast_val:10")
16
+ prods.must_include(@ducky)
17
+ end
18
+
19
+ it "casts Date when a cast function is provided and is <=" do
20
+ prods = Product.scopes_for_query("cast_val:<=11")
21
+ prods.must_include(@ducky)
22
+ end
16
23
 
24
+ it "casts Date when a cast function is provided and is >" do
25
+ prods = Product.scopes_for_query("cast_val:>9")
26
+ debugger
17
27
  prods.must_include(@ducky)
18
28
  end
19
29
  end
data/specs/spec_helper.rb CHANGED
@@ -44,7 +44,7 @@ class User < ActiveRecord::Base
44
44
  search.field :substringed_last_name, :attribute => "last_name", :match_substring => true
45
45
  search.field :available_product, :type => :boolean,
46
46
  :attribute => "products.available",
47
- :include => :products
47
+ :joins => :products
48
48
 
49
49
  search.keyword :aggregate_user do
50
50
  select(arel_table[:id]).
@@ -65,7 +65,7 @@ class Product < ActiveRecord::Base
65
65
  search.text_search "products.name", "products.value"
66
66
 
67
67
  search.field :name, :regex => true, :wildcard => true
68
- search.field :cast_val, :type => :decimal, :cast => lambda { |v| 10 }, :attribute => :value
68
+ search.field :cast_val, :type => :decimal, :cast => lambda { |v| v }, :attribute => :value
69
69
  search.field :value, :type => :decimal
70
70
  search.field :available, :type => :boolean
71
71
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: birddog
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.0.4
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-02-01 00:00:00.000000000Z
12
+ date: 2012-02-23 00:00:00.000000000Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: minitest
16
- requirement: &22249780 !ruby/object:Gem::Requirement
16
+ requirement: &24507660 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ! '>='
@@ -21,10 +21,10 @@ dependencies:
21
21
  version: '0'
22
22
  type: :development
23
23
  prerelease: false
24
- version_requirements: *22249780
24
+ version_requirements: *24507660
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: rake
27
- requirement: &22249280 !ruby/object:Gem::Requirement
27
+ requirement: &24507020 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - ! '>='
@@ -32,10 +32,10 @@ dependencies:
32
32
  version: '0'
33
33
  type: :development
34
34
  prerelease: false
35
- version_requirements: *22249280
35
+ version_requirements: *24507020
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: sqlite3-ruby
38
- requirement: &22248740 !ruby/object:Gem::Requirement
38
+ requirement: &24506140 !ruby/object:Gem::Requirement
39
39
  none: false
40
40
  requirements:
41
41
  - - ! '>='
@@ -43,10 +43,10 @@ dependencies:
43
43
  version: '0'
44
44
  type: :development
45
45
  prerelease: false
46
- version_requirements: *22248740
46
+ version_requirements: *24506140
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: chronic
49
- requirement: &22238660 !ruby/object:Gem::Requirement
49
+ requirement: &24505460 !ruby/object:Gem::Requirement
50
50
  none: false
51
51
  requirements:
52
52
  - - ! '>='
@@ -54,10 +54,10 @@ dependencies:
54
54
  version: '0'
55
55
  type: :runtime
56
56
  prerelease: false
57
- version_requirements: *22238660
57
+ version_requirements: *24505460
58
58
  - !ruby/object:Gem::Dependency
59
59
  name: activerecord
60
- requirement: &22238140 !ruby/object:Gem::Requirement
60
+ requirement: &24504040 !ruby/object:Gem::Requirement
61
61
  none: false
62
62
  requirements:
63
63
  - - ! '>='
@@ -65,7 +65,29 @@ dependencies:
65
65
  version: '0'
66
66
  type: :runtime
67
67
  prerelease: false
68
- version_requirements: *22238140
68
+ version_requirements: *24504040
69
+ - !ruby/object:Gem::Dependency
70
+ name: activesupport
71
+ requirement: &24503320 !ruby/object:Gem::Requirement
72
+ none: false
73
+ requirements:
74
+ - - ! '>='
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ type: :runtime
78
+ prerelease: false
79
+ version_requirements: *24503320
80
+ - !ruby/object:Gem::Dependency
81
+ name: squeel
82
+ requirement: &24502200 !ruby/object:Gem::Requirement
83
+ none: false
84
+ requirements:
85
+ - - ! '>='
86
+ - !ruby/object:Gem::Version
87
+ version: '0'
88
+ type: :runtime
89
+ prerelease: false
90
+ version_requirements: *24502200
69
91
  description: Seeeeeeeee Readme
70
92
  email:
71
93
  - brandonsdewitt@gmail.com
@@ -79,6 +101,7 @@ files:
79
101
  - Readme.rdoc
80
102
  - birddog.gemspec
81
103
  - lib/birddog.rb
104
+ - lib/birddog/birddog.rb
82
105
  - lib/birddog/boolean_expression.rb
83
106
  - lib/birddog/date_expression.rb
84
107
  - lib/birddog/field_conditions.rb
@@ -90,7 +113,6 @@ files:
90
113
  - specs/field_conditions_spec.rb
91
114
  - specs/spec_helper.rb
92
115
  - specs/support/minitest_matchers.rb
93
- - specs/test.db
94
116
  homepage: ''
95
117
  licenses: []
96
118
  post_install_message:
@@ -105,7 +127,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
105
127
  version: '0'
106
128
  segments:
107
129
  - 0
108
- hash: -2231863976579242335
130
+ hash: -387631044094105301
109
131
  required_rubygems_version: !ruby/object:Gem::Requirement
110
132
  none: false
111
133
  requirements:
@@ -114,7 +136,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
114
136
  version: '0'
115
137
  segments:
116
138
  - 0
117
- hash: -2231863976579242335
139
+ hash: -387631044094105301
118
140
  requirements: []
119
141
  rubyforge_project: birddog
120
142
  rubygems_version: 1.8.10
data/specs/test.db DELETED
Binary file