birddog 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/.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