wherewolf 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ./
3
3
  specs:
4
- wherewolf (0.1.0)
4
+ wherewolf (0.2.0)
5
5
  arel
6
6
  parslet
7
7
 
data/README.rdoc CHANGED
@@ -1,30 +1,32 @@
1
1
  = wherewolf
2
2
 
3
+ Makes adding filtering and searching to your REST API crazy easy.
4
+
3
5
  == Problem
4
6
 
5
7
  Most RESTful APIs expose a "/index" endpoint that return all of objects at a given endpoint. That is fine until you need the ability to filter them.
6
8
 
7
9
  Consider the following scenario:
8
10
 
9
- /companies.json Return all the companies.
11
+ /players.json
10
12
 
11
- But what if I want only companies that are active? Most developers would simply add a query parameter like so:
13
+ But what if I want only players that are active? Most developers would simply add a query parameter like so:
12
14
 
13
- /companies.json?active=true
15
+ /players.json?active=true
14
16
 
15
- Ok, now what if you only want companies created after the first of January 2012? Maybe:
17
+ Ok, now what if you only want players capped after the first of January 2012? Maybe:
16
18
 
17
- /companies.json?created_after=2012-01-01
19
+ /players.json?first_cap=2012-01-01
18
20
 
19
21
  Yeah, great - but it doesn't really scale. Wouldn't if be better if we could do something like this?
20
22
 
21
- /companies.json?where=active%20%3D%20true%20%26%26%20created_at%20%3E%3D%202012-01-01
23
+ /players.json?where=active%20%3D%20true%20%26%26%20first_cap%20%3E%3D%202012-01-01
22
24
 
23
- Ok, it doesn't read amazingly, but this is an API, so encoding that stuff is trivial for the client. For those of you that doesn't speak URI-coded string that is the same as:
25
+ Ok, it doesn't read amazingly, but this is an API, so encoding that stuff is trivial for the client. For those of you that doesn't speak URI-encoded string that is the same as:
24
26
 
25
- active = true && created_at >= 2012-01-01
27
+ active = true && first_cap >= 2012-01-01
26
28
 
27
- Wherewolf will take that string and converts it in to AREL, so your clients can run arbitary queries against your API.
29
+ Wherewolf will take that string and converts it in to ARel, so your clients can run arbitary queries against your API.
28
30
 
29
31
  == Get started
30
32
 
@@ -32,29 +34,70 @@ The easiest way is to use Bundler:
32
34
 
33
35
  gem 'wherewolf'
34
36
 
35
- == Where we are at
37
+ Then for every model that you want to by queryable, do this:
36
38
 
37
- What Works:
38
-
39
- * &&
40
- * ||
41
- * =
42
- * !=
43
- * <
44
- * <=
45
- * >
46
- * >=
47
- * Parenthesis
48
-
49
- Need to implement:
50
-
51
- * Aliases such for operators, such as 'and', 'or' etc
52
- * Allow from_query to nested (ie Player.where('first_cap < 2000-01-01').from_query('active = true')
53
- * More edge case testing
39
+ class Player < ActiveRecord::Base
40
+ has_query_parsing
41
+ end
42
+
43
+ This will add the "from_query" method, which you pass your query string in to.
54
44
 
55
45
  == Example
56
46
 
47
+ For a real-life, running example, check out: http://wherewolf.herokuapp.com/
48
+
57
49
  player = Player.from_query("(position = wing || position = lock) && first_cap < 1905-01-01").order('first_cap')
50
+ # Returns all players that play 'wing' or 'lock', and played before 1905-01-01
51
+
52
+ player = Player.from_query('name = "John Eales"')
53
+ # Returns all players names 'John Eales'
54
+
55
+ player = Player.from_query("first_cap >= 1905-01-01 && active = false")
56
+ # Returns all inactitve players that played after 1905-01-01.
57
+
58
+ player = Player.from_query("first_cap != null")
59
+ # Returns all players who have received their first cap (ie first_cap is NOT nil)
60
+
61
+ player = Player.from_query('name ~= "Peter%"')
62
+ # Returns all players who's name starts with Peter
63
+
64
+ As you can see, from_query returns an ARel object, so you chain other statements to it. Please note, though: at the moment from_query needs to be the first in the chain.
65
+
66
+ == Errors
67
+
68
+ At the moment, error handling is very primitive. Just capture
69
+
70
+ Wherewolf::ParseError
71
+
72
+ You can print out a simple error message like so
73
+
74
+ begin
75
+ Player.from_query('name ~= "Patrick%" || (position = "fail)')
76
+ rescue Wherewolf::ParseError => e
77
+ puts e.error_message
78
+ end
79
+
80
+ Will print out
81
+
82
+ Parsing error occured at character 28
83
+
84
+ You can get the character number by:
85
+
86
+ begin
87
+ Player.from_query('name ~= "Patrick%" || (position = "fail)')
88
+ rescue Wherewolf::ParseError => e
89
+ e.position # This value will be 28
90
+ end
91
+
92
+ == To Do
93
+
94
+ * Better error messages (Give a clue as to why parsing failed)
95
+ * Aliases such for operators, such as 'and', 'or' etc
96
+ * Allow single quotes around strings
97
+ * Allow from_query to nested (ie Player.where('first_cap < 2000-01-01').from_query('active = true')
98
+ * More edge case testing
99
+ * Abillity to filter columns that are searchable
100
+ * Ability to call named scopes
58
101
 
59
102
  == Contributing to wherewolf
60
103
 
data/Rakefile CHANGED
@@ -18,11 +18,11 @@ Jeweler::Tasks.new do |gem|
18
18
  gem.name = "wherewolf"
19
19
  gem.homepage = "http://github.com/madpilot/wherewolf"
20
20
  gem.license = "MIT"
21
- gem.summary = %Q{Query parser that converts search terms to AREL for use in APIs}
22
- gem.description = %Q{Wherewolf allows you to consume search terms as strings without worrying about database injections. It parses the query and converts it into AREL. It's great for creating filterable REST APIs}
21
+ gem.summary = %Q{Makes filtering and searching to your REST API crazy easy.}
22
+ gem.description = %Q{Wherewolf allows you to consume search terms as strings without worrying about database injections. It parses the query and converts it into ARel. It's great for creating filterable REST APIs.}
23
23
  gem.email = "myles@madpilot.com.au"
24
24
  gem.authors = ["Myles Eftos"]
25
- gem.version = "0.1.0"
25
+ gem.version = "0.2.0"
26
26
  # dependencies defined in Gemfile
27
27
  end
28
28
  Jeweler::RubygemsDotOrgTasks.new
data/lib/wherewolf.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  require File.join(File.dirname(__FILE__), 'wherewolf', 'parser.rb')
2
2
  require File.join(File.dirname(__FILE__), 'wherewolf', 'processor.rb')
3
+ require File.join(File.dirname(__FILE__), 'wherewolf', 'parse_error.rb')
3
4
 
4
5
  module Wherewolf
5
6
  def self.included(base)
@@ -0,0 +1,20 @@
1
+ module Wherewolf
2
+ class ParseError < Parslet::ParseFailed
3
+ attr_reader :parent
4
+ def initialize(parent)
5
+ @parent = parent
6
+ end
7
+
8
+ def position
9
+ parent.cause.source.pos
10
+ end
11
+
12
+ def error_message
13
+ "Parsing error occured at character #{position}"
14
+ end
15
+
16
+ def to_s
17
+ error_message
18
+ end
19
+ end
20
+ end
@@ -10,6 +10,7 @@ module Wherewolf
10
10
  # Comparisons
11
11
  rule(:eq) { str('=') }
12
12
  rule(:not_eq) { str('!=') }
13
+ rule(:matches) { str('~=') }
13
14
  rule(:lt) { str('<') }
14
15
  rule(:lteq) { str('<=') }
15
16
  rule(:gt) { str('>') }
@@ -33,16 +34,17 @@ module Wherewolf
33
34
  end
34
35
  rule(:literal) { match('[a-zA-Z0-9\-_]').repeat(1) }
35
36
  rule(:identifier) { null | boolean | number | double_quote_string | literal.as(:string) }
36
-
37
+
37
38
  # Grammar
38
39
  rule(:compare_eq) { (literal.as(:left) >> space? >> eq >> space? >> identifier.as(:right)).as(:eq) }
39
40
  rule(:compare_not_eq) { (literal.as(:left) >> space? >> not_eq >> space? >> identifier.as(:right)).as(:not_eq) }
41
+ rule(:compare_matches) { (literal.as(:left) >> space? >> matches >> space? >> identifier.as(:right)).as(:matches) }
40
42
  rule(:compare_lt) { (literal.as(:left) >> space? >> lt >> space? >> identifier.as(:right)).as(:lt) }
41
43
  rule(:compare_lteq) { (literal.as(:left) >> space? >> lteq >> space? >> identifier.as(:right)).as(:lteq) }
42
44
  rule(:compare_gt) { (literal.as(:left) >> space? >> gt >> space? >> identifier.as(:right)).as(:gt) }
43
45
  rule(:compare_gteq) { (literal.as(:left) >> space? >> gteq >> space? >> identifier.as(:right)).as(:gteq) }
44
46
 
45
- rule(:compare) { compare_eq | compare_not_eq | compare_lteq | compare_lt | compare_gteq | compare_gt }
47
+ rule(:compare) { compare_eq | compare_not_eq | compare_matches | compare_lteq | compare_lt | compare_gteq | compare_gt }
46
48
 
47
49
  rule(:primary) { left_parenthesis >> space? >> or_operation >> space? >> right_parenthesis | compare }
48
50
  rule(:and_operation) { (primary.as(:left) >> space? >> and_operator >> space? >> and_operation.as(:right)).as(:and) | primary }
@@ -8,9 +8,13 @@ module Wherewolf
8
8
  end
9
9
 
10
10
  def parse(model, query)
11
- ast = Wherewolf::Parser.new.parse(query)
12
- table = model.arel_table
13
- model.where(process(ast, table))
11
+ begin
12
+ ast = Wherewolf::Parser.new.parse(query)
13
+ table = model.arel_table
14
+ model.where(process(ast, table))
15
+ rescue Parslet::ParseFailed => error
16
+ raise Wherewolf::ParseError, error
17
+ end
14
18
  end
15
19
 
16
20
  def process(ast, table)
@@ -35,6 +39,10 @@ protected
35
39
  table[ast[:left].to_sym].not_eq(parse_value(ast[:right]))
36
40
  end
37
41
 
42
+ def process_matches(ast, table)
43
+ table[ast[:left].to_sym].matches(parse_value(ast[:right]))
44
+ end
45
+
38
46
  def process_lt(ast, table)
39
47
  table[ast[:left].to_sym].lt(parse_value(ast[:right]))
40
48
  end
@@ -0,0 +1,9 @@
1
+ require 'helper'
2
+
3
+ class ParseErrorTest < Test::Unit::TestCase
4
+ context 'ParseError' do
5
+ should 'be a child of Parslet::ParseFailed' do
6
+ Wherewolf::ParseError.is_a?(Parslet::ParseFailed)
7
+ end
8
+ end
9
+ end
data/test/parser_test.rb CHANGED
@@ -76,6 +76,25 @@ class ParserTest < Test::Unit::TestCase
76
76
  end
77
77
  end
78
78
 
79
+ context "matches" do
80
+ should 'parse [op1]~=[op2]' do
81
+ result = @parser.parse('name~="Myles"')
82
+ assert_equal( { :matches => { :left => "name", :right => { :string => "Myles" } } }, result)
83
+ end
84
+ should 'parse [op1] ~=[op2]' do
85
+ result = @parser.parse('name ~="Myles"')
86
+ assert_equal( { :matches => { :left => "name", :right => { :string => "Myles" } } }, result)
87
+ end
88
+ should 'parse [op1]~= [op2]' do
89
+ result = @parser.parse('name~= "Myles"')
90
+ assert_equal( { :matches => { :left => "name", :right => { :string => "Myles" } } }, result)
91
+ end
92
+ should 'parse [op1] ~= [op2]' do
93
+ result = @parser.parse('name ~= "Myles"')
94
+ assert_equal( { :matches => { :left => "name", :right => { :string => "Myles" } } }, result)
95
+ end
96
+ end
97
+
79
98
  context "less than" do
80
99
  should 'parse [op1]<[op2]' do
81
100
  result = @parser.parse("size<12")
@@ -27,6 +27,40 @@ class ProcessorTest < Test::Unit::TestCase
27
27
  end
28
28
 
29
29
  context 'Parsing' do
30
+ context 'Error' do
31
+ should 'be raised is there is a parser error' do
32
+ assert_raise Wherewolf::ParseError do
33
+ Player.from_query('name ~= "Patrick%" || (position = "fail)')
34
+ end
35
+ end
36
+
37
+ should 'allow retrieval of the error position' do
38
+ begin
39
+ Player.from_query('name ~= "Patrick%" || (position = "fail)')
40
+ rescue Wherewolf::ParseError => e
41
+ assert_equal 28, e.position
42
+ end
43
+ end
44
+
45
+
46
+ should 'show a nice debug error' do
47
+ begin
48
+ Player.from_query('name ~= "Patrick%" || (position = "fail)')
49
+ rescue Wherewolf::ParseError => e
50
+ assert_equal "Parsing error occured at character 28", e.error_message
51
+ end
52
+ end
53
+
54
+ should 'to_s should print out nice error' do
55
+ begin
56
+ Player.from_query('name ~= "Patrick%" || (position = "fail)')
57
+ rescue Wherewolf::ParseError => e
58
+ assert_equal "Parsing error occured at character 28", e.to_s
59
+ end
60
+ end
61
+
62
+ end
63
+
30
64
  should 'construct simple boolean statements' do
31
65
  player = Player.from_query('name = "Charlie Ellis"')
32
66
  assert_equal 1, player.count
@@ -47,6 +81,12 @@ class ProcessorTest < Test::Unit::TestCase
47
81
  assert_equal "Charlie Redwood", player[2].name
48
82
  end
49
83
 
84
+ should 'handle matches' do
85
+ player = Player.from_query('name ~= "James%"')
86
+ assert_equal 1, player.count
87
+ assert_equal "James Slipper", player.first.name
88
+ end
89
+
50
90
  should 'handle nulls' do
51
91
  player = Player.from_query("first_cap = null")
52
92
  assert_equal 1, player.count
data/wherewolf.gemspec CHANGED
@@ -5,12 +5,12 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = "wherewolf"
8
- s.version = "0.1.0"
8
+ s.version = "0.2.0"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Myles Eftos"]
12
12
  s.date = "2012-10-10"
13
- s.description = "Wherewolf allows you to consume search terms as strings without worrying about database injections. It parses the query and converts it into AREL. It's great for creating filterable REST APIs"
13
+ s.description = "Wherewolf allows you to consume search terms as strings without worrying about database injections. It parses the query and converts it into ARel. It's great for creating filterable REST APIs."
14
14
  s.email = "myles@madpilot.com.au"
15
15
  s.extra_rdoc_files = [
16
16
  "LICENSE.txt",
@@ -25,10 +25,12 @@ Gem::Specification.new do |s|
25
25
  "README.rdoc",
26
26
  "Rakefile",
27
27
  "lib/wherewolf.rb",
28
+ "lib/wherewolf/parse_error.rb",
28
29
  "lib/wherewolf/parser.rb",
29
30
  "lib/wherewolf/processor.rb",
30
31
  "lib/wherewolf/railtie.rb",
31
32
  "test/helper.rb",
33
+ "test/parse_error_test.rb",
32
34
  "test/parser_test.rb",
33
35
  "test/processor_test.rb",
34
36
  "test/railtie_test.rb",
@@ -38,7 +40,7 @@ Gem::Specification.new do |s|
38
40
  s.licenses = ["MIT"]
39
41
  s.require_paths = ["lib"]
40
42
  s.rubygems_version = "1.8.15"
41
- s.summary = "Query parser that converts search terms to AREL for use in APIs"
43
+ s.summary = "Makes filtering and searching to your REST API crazy easy."
42
44
 
43
45
  if s.respond_to? :specification_version then
44
46
  s.specification_version = 3
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: wherewolf
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -13,7 +13,7 @@ date: 2012-10-10 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: arel
16
- requirement: &70230035647040 !ruby/object:Gem::Requirement
16
+ requirement: &70166778516180 !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: :runtime
23
23
  prerelease: false
24
- version_requirements: *70230035647040
24
+ version_requirements: *70166778516180
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: parslet
27
- requirement: &70230035645880 !ruby/object:Gem::Requirement
27
+ requirement: &70166778514340 !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: :runtime
34
34
  prerelease: false
35
- version_requirements: *70230035645880
35
+ version_requirements: *70166778514340
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: shoulda
38
- requirement: &70230035645020 !ruby/object:Gem::Requirement
38
+ requirement: &70166778512340 !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: *70230035645020
46
+ version_requirements: *70166778512340
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: rdoc
49
- requirement: &70230035658520 !ruby/object:Gem::Requirement
49
+ requirement: &70166778511320 !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: :development
56
56
  prerelease: false
57
- version_requirements: *70230035658520
57
+ version_requirements: *70166778511320
58
58
  - !ruby/object:Gem::Dependency
59
59
  name: bundler
60
- requirement: &70230035655100 !ruby/object:Gem::Requirement
60
+ requirement: &70166778510520 !ruby/object:Gem::Requirement
61
61
  none: false
62
62
  requirements:
63
63
  - - ! '>='
@@ -65,10 +65,10 @@ dependencies:
65
65
  version: '0'
66
66
  type: :development
67
67
  prerelease: false
68
- version_requirements: *70230035655100
68
+ version_requirements: *70166778510520
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: jeweler
71
- requirement: &70230035654260 !ruby/object:Gem::Requirement
71
+ requirement: &70166778509620 !ruby/object:Gem::Requirement
72
72
  none: false
73
73
  requirements:
74
74
  - - ! '>='
@@ -76,10 +76,10 @@ dependencies:
76
76
  version: '0'
77
77
  type: :development
78
78
  prerelease: false
79
- version_requirements: *70230035654260
79
+ version_requirements: *70166778509620
80
80
  - !ruby/object:Gem::Dependency
81
81
  name: guard
82
- requirement: &70230035653240 !ruby/object:Gem::Requirement
82
+ requirement: &70166778525100 !ruby/object:Gem::Requirement
83
83
  none: false
84
84
  requirements:
85
85
  - - ! '>='
@@ -87,10 +87,10 @@ dependencies:
87
87
  version: '0'
88
88
  type: :development
89
89
  prerelease: false
90
- version_requirements: *70230035653240
90
+ version_requirements: *70166778525100
91
91
  - !ruby/object:Gem::Dependency
92
92
  name: guard-test
93
- requirement: &70230035681240 !ruby/object:Gem::Requirement
93
+ requirement: &70166778524220 !ruby/object:Gem::Requirement
94
94
  none: false
95
95
  requirements:
96
96
  - - ! '>='
@@ -98,10 +98,10 @@ dependencies:
98
98
  version: '0'
99
99
  type: :development
100
100
  prerelease: false
101
- version_requirements: *70230035681240
101
+ version_requirements: *70166778524220
102
102
  - !ruby/object:Gem::Dependency
103
103
  name: simplecov
104
- requirement: &70230035677480 !ruby/object:Gem::Requirement
104
+ requirement: &70166778522920 !ruby/object:Gem::Requirement
105
105
  none: false
106
106
  requirements:
107
107
  - - ! '>='
@@ -109,10 +109,10 @@ dependencies:
109
109
  version: '0'
110
110
  type: :development
111
111
  prerelease: false
112
- version_requirements: *70230035677480
112
+ version_requirements: *70166778522920
113
113
  - !ruby/object:Gem::Dependency
114
114
  name: sqlite3
115
- requirement: &70230035689500 !ruby/object:Gem::Requirement
115
+ requirement: &70166778521500 !ruby/object:Gem::Requirement
116
116
  none: false
117
117
  requirements:
118
118
  - - ! '>='
@@ -120,10 +120,10 @@ dependencies:
120
120
  version: '0'
121
121
  type: :development
122
122
  prerelease: false
123
- version_requirements: *70230035689500
123
+ version_requirements: *70166778521500
124
124
  - !ruby/object:Gem::Dependency
125
125
  name: rails
126
- requirement: &70230035685280 !ruby/object:Gem::Requirement
126
+ requirement: &70166778520140 !ruby/object:Gem::Requirement
127
127
  none: false
128
128
  requirements:
129
129
  - - ! '>='
@@ -131,10 +131,10 @@ dependencies:
131
131
  version: '0'
132
132
  type: :development
133
133
  prerelease: false
134
- version_requirements: *70230035685280
134
+ version_requirements: *70166778520140
135
135
  - !ruby/object:Gem::Dependency
136
136
  name: wherewolf
137
- requirement: &70230035683060 !ruby/object:Gem::Requirement
137
+ requirement: &70166778519320 !ruby/object:Gem::Requirement
138
138
  none: false
139
139
  requirements:
140
140
  - - ! '>='
@@ -142,10 +142,10 @@ dependencies:
142
142
  version: '0'
143
143
  type: :development
144
144
  prerelease: false
145
- version_requirements: *70230035683060
145
+ version_requirements: *70166778519320
146
146
  description: Wherewolf allows you to consume search terms as strings without worrying
147
- about database injections. It parses the query and converts it into AREL. It's great
148
- for creating filterable REST APIs
147
+ about database injections. It parses the query and converts it into ARel. It's great
148
+ for creating filterable REST APIs.
149
149
  email: myles@madpilot.com.au
150
150
  executables: []
151
151
  extensions: []
@@ -161,10 +161,12 @@ files:
161
161
  - README.rdoc
162
162
  - Rakefile
163
163
  - lib/wherewolf.rb
164
+ - lib/wherewolf/parse_error.rb
164
165
  - lib/wherewolf/parser.rb
165
166
  - lib/wherewolf/processor.rb
166
167
  - lib/wherewolf/railtie.rb
167
168
  - test/helper.rb
169
+ - test/parse_error_test.rb
168
170
  - test/parser_test.rb
169
171
  - test/processor_test.rb
170
172
  - test/railtie_test.rb
@@ -184,7 +186,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
184
186
  version: '0'
185
187
  segments:
186
188
  - 0
187
- hash: 3955971048555964075
189
+ hash: -122893438368244231
188
190
  required_rubygems_version: !ruby/object:Gem::Requirement
189
191
  none: false
190
192
  requirements:
@@ -196,5 +198,5 @@ rubyforge_project:
196
198
  rubygems_version: 1.8.15
197
199
  signing_key:
198
200
  specification_version: 3
199
- summary: Query parser that converts search terms to AREL for use in APIs
201
+ summary: Makes filtering and searching to your REST API crazy easy.
200
202
  test_files: []