wherewolf 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
data/Gemfile ADDED
@@ -0,0 +1,17 @@
1
+ source "http://rubygems.org"
2
+
3
+ gem "arel"
4
+ gem "parslet"
5
+
6
+ group :development do
7
+ gem "shoulda"
8
+ gem "rdoc"
9
+ gem "bundler"
10
+ gem "jeweler"
11
+ gem "guard"
12
+ gem "guard-test"
13
+ gem "simplecov", :require => false
14
+ gem "sqlite3"
15
+ gem "rails"
16
+ gem "wherewolf", :path => "./"
17
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,140 @@
1
+ PATH
2
+ remote: ./
3
+ specs:
4
+ wherewolf (0.1.0)
5
+ arel
6
+ parslet
7
+
8
+ GEM
9
+ remote: http://rubygems.org/
10
+ specs:
11
+ actionmailer (3.2.8)
12
+ actionpack (= 3.2.8)
13
+ mail (~> 2.4.4)
14
+ actionpack (3.2.8)
15
+ activemodel (= 3.2.8)
16
+ activesupport (= 3.2.8)
17
+ builder (~> 3.0.0)
18
+ erubis (~> 2.7.0)
19
+ journey (~> 1.0.4)
20
+ rack (~> 1.4.0)
21
+ rack-cache (~> 1.2)
22
+ rack-test (~> 0.6.1)
23
+ sprockets (~> 2.1.3)
24
+ activemodel (3.2.8)
25
+ activesupport (= 3.2.8)
26
+ builder (~> 3.0.0)
27
+ activerecord (3.2.8)
28
+ activemodel (= 3.2.8)
29
+ activesupport (= 3.2.8)
30
+ arel (~> 3.0.2)
31
+ tzinfo (~> 0.3.29)
32
+ activeresource (3.2.8)
33
+ activemodel (= 3.2.8)
34
+ activesupport (= 3.2.8)
35
+ activesupport (3.2.8)
36
+ i18n (~> 0.6)
37
+ multi_json (~> 1.0)
38
+ arel (3.0.2)
39
+ blankslate (2.1.2.4)
40
+ builder (3.0.3)
41
+ erubis (2.7.0)
42
+ ffi (1.1.5)
43
+ git (1.2.5)
44
+ guard (1.3.2)
45
+ listen (>= 0.4.2)
46
+ thor (>= 0.14.6)
47
+ guard-test (0.5.0)
48
+ guard (>= 1.1.0)
49
+ test-unit (~> 2.2)
50
+ hike (1.2.1)
51
+ i18n (0.6.1)
52
+ jeweler (1.8.4)
53
+ bundler (~> 1.0)
54
+ git (>= 1.2.5)
55
+ rake
56
+ rdoc
57
+ journey (1.0.4)
58
+ json (1.7.5)
59
+ listen (0.4.7)
60
+ rb-fchange (~> 0.0.5)
61
+ rb-fsevent (~> 0.9.1)
62
+ rb-inotify (~> 0.8.8)
63
+ mail (2.4.4)
64
+ i18n (>= 0.4.0)
65
+ mime-types (~> 1.16)
66
+ treetop (~> 1.4.8)
67
+ mime-types (1.19)
68
+ multi_json (1.3.6)
69
+ parslet (1.4.0)
70
+ blankslate (~> 2.0)
71
+ polyglot (0.3.3)
72
+ rack (1.4.1)
73
+ rack-cache (1.2)
74
+ rack (>= 0.4)
75
+ rack-ssl (1.3.2)
76
+ rack
77
+ rack-test (0.6.1)
78
+ rack (>= 1.0)
79
+ rails (3.2.8)
80
+ actionmailer (= 3.2.8)
81
+ actionpack (= 3.2.8)
82
+ activerecord (= 3.2.8)
83
+ activeresource (= 3.2.8)
84
+ activesupport (= 3.2.8)
85
+ bundler (~> 1.0)
86
+ railties (= 3.2.8)
87
+ railties (3.2.8)
88
+ actionpack (= 3.2.8)
89
+ activesupport (= 3.2.8)
90
+ rack-ssl (~> 1.3.2)
91
+ rake (>= 0.8.7)
92
+ rdoc (~> 3.4)
93
+ thor (>= 0.14.6, < 2.0)
94
+ rake (0.9.2.2)
95
+ rb-fchange (0.0.5)
96
+ ffi
97
+ rb-fsevent (0.9.1)
98
+ rb-inotify (0.8.8)
99
+ ffi (>= 0.5.0)
100
+ rdoc (3.12)
101
+ json (~> 1.4)
102
+ shoulda (3.3.0)
103
+ shoulda-context (~> 1.0)
104
+ shoulda-matchers (~> 1.4)
105
+ shoulda-context (1.0.0)
106
+ shoulda-matchers (1.4.0)
107
+ activesupport (>= 3.0.0)
108
+ simplecov (0.6.4)
109
+ multi_json (~> 1.0)
110
+ simplecov-html (~> 0.5.3)
111
+ simplecov-html (0.5.3)
112
+ sprockets (2.1.3)
113
+ hike (~> 1.2)
114
+ rack (~> 1.0)
115
+ tilt (~> 1.1, != 1.3.0)
116
+ sqlite3 (1.3.6)
117
+ test-unit (2.5.2)
118
+ thor (0.16.0)
119
+ tilt (1.3.3)
120
+ treetop (1.4.10)
121
+ polyglot
122
+ polyglot (>= 0.3.1)
123
+ tzinfo (0.3.33)
124
+
125
+ PLATFORMS
126
+ ruby
127
+
128
+ DEPENDENCIES
129
+ arel
130
+ bundler
131
+ guard
132
+ guard-test
133
+ jeweler
134
+ parslet
135
+ rails
136
+ rdoc
137
+ shoulda
138
+ simplecov
139
+ sqlite3
140
+ wherewolf!
data/Guardfile ADDED
@@ -0,0 +1,4 @@
1
+ guard :test do
2
+ watch(%r{^lib/wherewolf/(.+)\.rb$}) { |m| "test/#{m[1]}_test.rb" }
3
+ watch(%r{^test/(.+)_test\.rb$}) { |m| "lib/wherewolf/#{m[1]}.rb" }
4
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2012 Myles Eftos
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,72 @@
1
+ = wherewolf
2
+
3
+ == Problem
4
+
5
+ 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
+
7
+ Consider the following scenario:
8
+
9
+ /companies.json Return all the companies.
10
+
11
+ But what if I want only companies that are active? Most developers would simply add a query parameter like so:
12
+
13
+ /companies.json?active=true
14
+
15
+ Ok, now what if you only want companies created after the first of January 2012? Maybe:
16
+
17
+ /companies.json?created_after=2012-01-01
18
+
19
+ Yeah, great - but it doesn't really scale. Wouldn't if be better if we could do something like this?
20
+
21
+ /companies.json?where=active%20%3D%20true%20%26%26%20created_at%20%3E%3D%202012-01-01
22
+
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:
24
+
25
+ active = true && created_at >= 2012-01-01
26
+
27
+ Wherewolf will take that string and converts it in to AREL, so your clients can run arbitary queries against your API.
28
+
29
+ == Get started
30
+
31
+ The easiest way is to use Bundler:
32
+
33
+ gem 'wherewolf'
34
+
35
+ == Where we are at
36
+
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
54
+
55
+ == Example
56
+
57
+ player = Player.from_query("(position = wing || position = lock) && first_cap < 1905-01-01").order('first_cap')
58
+
59
+ == Contributing to wherewolf
60
+
61
+ * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet.
62
+ * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it.
63
+ * Fork the project.
64
+ * Start a feature/bugfix branch.
65
+ * Commit and push until you are happy with your contribution.
66
+ * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
67
+ * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
68
+
69
+ == Copyright
70
+
71
+ Copyright (c) 2012 Myles Eftos. See LICENSE.txt for
72
+ further details.
data/Rakefile ADDED
@@ -0,0 +1,47 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rubygems'
4
+ require 'bundler'
5
+ begin
6
+ Bundler.setup(:default, :development)
7
+ rescue Bundler::BundlerError => e
8
+ $stderr.puts e.message
9
+ $stderr.puts "Run `bundle install` to install missing gems"
10
+ exit e.status_code
11
+ end
12
+ require 'rake'
13
+
14
+ require 'jeweler'
15
+
16
+ Jeweler::Tasks.new do |gem|
17
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
18
+ gem.name = "wherewolf"
19
+ gem.homepage = "http://github.com/madpilot/wherewolf"
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}
23
+ gem.email = "myles@madpilot.com.au"
24
+ gem.authors = ["Myles Eftos"]
25
+ gem.version = "0.1.0"
26
+ # dependencies defined in Gemfile
27
+ end
28
+ Jeweler::RubygemsDotOrgTasks.new
29
+
30
+ require 'rake/testtask'
31
+ Rake::TestTask.new(:test) do |test|
32
+ test.libs << 'lib' << 'test'
33
+ test.pattern = 'test/*_test.rb'
34
+ test.verbose = true
35
+ end
36
+
37
+ task :default => :test
38
+
39
+ require 'rdoc/task'
40
+ Rake::RDocTask.new do |rdoc|
41
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
42
+
43
+ rdoc.rdoc_dir = 'rdoc'
44
+ rdoc.title = "wherewolf #{version}"
45
+ rdoc.rdoc_files.include('README*')
46
+ rdoc.rdoc_files.include('lib/**/*.rb')
47
+ end
@@ -0,0 +1,53 @@
1
+ require 'parslet'
2
+
3
+ module Wherewolf
4
+ class Parser < Parslet::Parser
5
+ rule(:space) { match('\s').repeat(1) }
6
+ rule(:space?) { space.maybe }
7
+ rule(:left_parenthesis) { str('(') }
8
+ rule(:right_parenthesis) { str(')') }
9
+
10
+ # Comparisons
11
+ rule(:eq) { str('=') }
12
+ rule(:not_eq) { str('!=') }
13
+ rule(:lt) { str('<') }
14
+ rule(:lteq) { str('<=') }
15
+ rule(:gt) { str('>') }
16
+ rule(:gteq) { str('>=') }
17
+
18
+ # Operators
19
+ rule(:and_operator) { str('&&') }
20
+ rule(:or_operator) { str('||') }
21
+
22
+ # Operand
23
+ rule(:null) { str("null").as(:nil) }
24
+ rule(:boolean) { str("true").as(:boolean) | str("false").as(:boolean) }
25
+ rule(:number) { match('[-+]?([0-9]*\.)?[0-9]').repeat(1).as(:number) }
26
+ rule(:double_quote_string) do
27
+ str('"') >>
28
+ (
29
+ (str('\\') >> any) |
30
+ (str('"').absent? >> any)
31
+ ).repeat.as(:string) >>
32
+ str('"')
33
+ end
34
+ rule(:literal) { match('[a-zA-Z0-9\-_]').repeat(1) }
35
+ rule(:identifier) { null | boolean | number | double_quote_string | literal.as(:string) }
36
+
37
+ # Grammar
38
+ rule(:compare_eq) { (literal.as(:left) >> space? >> eq >> space? >> identifier.as(:right)).as(:eq) }
39
+ rule(:compare_not_eq) { (literal.as(:left) >> space? >> not_eq >> space? >> identifier.as(:right)).as(:not_eq) }
40
+ rule(:compare_lt) { (literal.as(:left) >> space? >> lt >> space? >> identifier.as(:right)).as(:lt) }
41
+ rule(:compare_lteq) { (literal.as(:left) >> space? >> lteq >> space? >> identifier.as(:right)).as(:lteq) }
42
+ rule(:compare_gt) { (literal.as(:left) >> space? >> gt >> space? >> identifier.as(:right)).as(:gt) }
43
+ rule(:compare_gteq) { (literal.as(:left) >> space? >> gteq >> space? >> identifier.as(:right)).as(:gteq) }
44
+
45
+ rule(:compare) { compare_eq | compare_not_eq | compare_lteq | compare_lt | compare_gteq | compare_gt }
46
+
47
+ rule(:primary) { left_parenthesis >> space? >> or_operation >> space? >> right_parenthesis | compare }
48
+ rule(:and_operation) { (primary.as(:left) >> space? >> and_operator >> space? >> and_operation.as(:right)).as(:and) | primary }
49
+ rule(:or_operation) { (and_operation.as(:left) >> space? >> or_operator >> space? >> or_operation.as(:right)).as(:or) | and_operation }
50
+
51
+ root :or_operation
52
+ end
53
+ end
@@ -0,0 +1,66 @@
1
+ require 'arel'
2
+
3
+ module Wherewolf
4
+ class Processor
5
+ def self.parse(model, query)
6
+ instance = self.new
7
+ instance.parse(model, query)
8
+ end
9
+
10
+ def parse(model, query)
11
+ ast = Wherewolf::Parser.new.parse(query)
12
+ table = model.arel_table
13
+ model.where(process(ast, table))
14
+ end
15
+
16
+ def process(ast, table)
17
+ operation = ast.keys.first
18
+ self.send("process_#{operation}".to_sym, ast[operation], table) if self.respond_to?("process_#{operation}".to_sym)
19
+ end
20
+
21
+ protected
22
+ def process_and(ast, table)
23
+ process(ast[:left], table).and(process(ast[:right], table))
24
+ end
25
+
26
+ def process_or(ast, table)
27
+ process(ast[:left], table).or(process(ast[:right], table))
28
+ end
29
+
30
+ def process_eq(ast, table)
31
+ table[ast[:left].to_sym].eq(parse_value(ast[:right]))
32
+ end
33
+
34
+ def process_not_eq(ast, table)
35
+ table[ast[:left].to_sym].not_eq(parse_value(ast[:right]))
36
+ end
37
+
38
+ def process_lt(ast, table)
39
+ table[ast[:left].to_sym].lt(parse_value(ast[:right]))
40
+ end
41
+
42
+ def process_lteq(ast, table)
43
+ table[ast[:left].to_sym].lteq(parse_value(ast[:right]))
44
+ end
45
+
46
+ def process_gt(ast, table)
47
+ table[ast[:left].to_sym].gt(parse_value(ast[:right]))
48
+ end
49
+
50
+ def process_gteq(ast, table)
51
+ table[ast[:left].to_sym].gteq(parse_value(ast[:right]))
52
+ end
53
+
54
+ def parse_value(value)
55
+ type = value.keys.first
56
+ case type
57
+ when :nil
58
+ return nil
59
+ when :boolean
60
+ return value[:boolean] == "true"
61
+ else
62
+ return value[type].to_s
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,9 @@
1
+ module Wherewolf
2
+ class Railtie < Rails::Railtie
3
+ initializer 'wherewolf' do |app|
4
+ ActiveSupport.on_load :active_record do
5
+ ActiveRecord::Base.send :include, Wherewolf
6
+ end
7
+ end
8
+ end
9
+ end
data/lib/wherewolf.rb ADDED
@@ -0,0 +1,22 @@
1
+ require File.join(File.dirname(__FILE__), 'wherewolf', 'parser.rb')
2
+ require File.join(File.dirname(__FILE__), 'wherewolf', 'processor.rb')
3
+
4
+ module Wherewolf
5
+ def self.included(base)
6
+ base.send :extend, ClassMethods
7
+ end
8
+
9
+ module ClassMethods
10
+ def has_query_parsing(options = {})
11
+ self.extend QueryMethods
12
+ end
13
+ end
14
+
15
+ module QueryMethods
16
+ def from_query(query)
17
+ Wherewolf::Processor.parse(self, query)
18
+ end
19
+ end
20
+ end
21
+
22
+ require File.join(File.dirname(__FILE__), 'wherewolf', 'railtie.rb') if defined?(Rails::Railtie)
data/test/helper.rb ADDED
@@ -0,0 +1,62 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ begin
4
+ Bundler.setup(:default, :development)
5
+ rescue Bundler::BundlerError => e
6
+ $stderr.puts e.message
7
+ $stderr.puts "Run `bundle install` to install missing gems"
8
+ exit e.status_code
9
+ end
10
+ require 'simplecov'
11
+ SimpleCov.command_name 'Unit Tests'
12
+ SimpleCov.start do
13
+ add_filter "/test/"
14
+ end
15
+
16
+ require 'test/unit'
17
+ require 'shoulda'
18
+
19
+ require 'thread'
20
+ require 'rails/all'
21
+
22
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
23
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
24
+ require 'wherewolf'
25
+
26
+ class TestApp < Rails::Application
27
+ config.root = File.dirname(__FILE__)
28
+ end
29
+
30
+ Rails.application = TestApp
31
+ Wherewolf::Railtie.initializers.first.run(Rails.application)
32
+ ActiveRecord::Migration.verbose = false
33
+
34
+ class AddUsers < ActiveRecord::Migration
35
+ def up
36
+ create_table :players do |t|
37
+ t.string :name
38
+ t.string :position
39
+ t.boolean :active
40
+ t.date :first_cap
41
+ end
42
+
43
+ create_table :teams do |t|
44
+ t.string :team
45
+ end
46
+ end
47
+
48
+ def down
49
+ drop_table :users
50
+ drop_table :teams
51
+ end
52
+ end
53
+
54
+ class Player < ActiveRecord::Base
55
+ has_query_parsing
56
+ end
57
+
58
+ class Team < ActiveRecord::Base
59
+ end
60
+
61
+ class Test::Unit::TestCase
62
+ end
@@ -0,0 +1,214 @@
1
+ require 'helper'
2
+
3
+ class ParserTest < Test::Unit::TestCase
4
+ context 'Parser' do
5
+ setup do
6
+ @parser = Wherewolf::Parser.new
7
+ end
8
+
9
+ context "operands" do
10
+ should 'parse [op1]=[literal]' do
11
+ result = @parser.parse('name=Myles')
12
+ assert_equal( { :eq => { :left => "name", :right => { :string => "Myles" } } }, result)
13
+ end
14
+ should 'parse [op1]="[string]"' do
15
+ result = @parser.parse('name="Myles Eftos"')
16
+ assert_equal( { :eq => { :left => "name", :right => { :string => "Myles Eftos" } } }, result)
17
+ end
18
+ should 'parse [op1]=null' do
19
+ result = @parser.parse('name=null')
20
+ assert_equal( { :eq => { :left => "name", :right => { :nil => "null" } } }, result)
21
+ end
22
+ should 'parse [op1]=true' do
23
+ result = @parser.parse('active=true')
24
+ assert_equal( { :eq => { :left => "active", :right => { :boolean => "true" } } }, result)
25
+ end
26
+ should 'parse [op1]=false' do
27
+ result = @parser.parse('active=false')
28
+ assert_equal( { :eq => { :left => "active", :right => { :boolean => "false" } } }, result)
29
+ end
30
+ should 'parse [op1]=[integer]' do
31
+ result = @parser.parse('count=10')
32
+ assert_equal( { :eq => { :left => "count", :right => { :number => "10" } } }, result)
33
+ end
34
+ should 'parse [op1]=[float]' do
35
+ result = @parser.parse('count=-10.3')
36
+ assert_equal( { :eq => { :left => "count", :right => { :number => "-10.3" } } }, result)
37
+ end
38
+ end
39
+
40
+ context "comparators" do
41
+ context "equals" do
42
+ should 'parse [op1]=[op2]' do
43
+ result = @parser.parse('name="Myles"')
44
+ assert_equal( { :eq => { :left => "name", :right => { :string => "Myles" } } }, result)
45
+ end
46
+ should 'parse [op1] =[op2]' do
47
+ result = @parser.parse('name ="Myles"')
48
+ assert_equal( { :eq => { :left => "name", :right => { :string => "Myles" } } }, result)
49
+ end
50
+ should 'parse [op1]= [op2]' do
51
+ result = @parser.parse('name= "Myles"')
52
+ assert_equal( { :eq => { :left => "name", :right => { :string => "Myles" } } }, result)
53
+ end
54
+ should 'parse [op1] = [op2]' do
55
+ result = @parser.parse('name = "Myles"')
56
+ assert_equal( { :eq => { :left => "name", :right => { :string => "Myles" } } }, result)
57
+ end
58
+ end
59
+
60
+ context "not equals" do
61
+ should 'parse [op1]!=[op2]' do
62
+ result = @parser.parse('name!="Myles Eftos"')
63
+ assert_equal( { :not_eq => { :left => "name", :right => { :string => 'Myles Eftos' } } }, result)
64
+ end
65
+ should 'parse [op1] !=[op2]' do
66
+ result = @parser.parse('name !="Myles Eftos"')
67
+ assert_equal( { :not_eq => { :left => "name", :right => { :string => 'Myles Eftos' } } }, result)
68
+ end
69
+ should 'parse [op1]!= [op2]' do
70
+ result = @parser.parse('name!= "Myles Eftos"')
71
+ assert_equal( { :not_eq => { :left => "name", :right => { :string => 'Myles Eftos' } } }, result)
72
+ end
73
+ should 'parse [op1] != [op2]' do
74
+ result = @parser.parse('name != "Myles Eftos"')
75
+ assert_equal( { :not_eq => { :left => "name", :right => { :string => 'Myles Eftos' } } }, result)
76
+ end
77
+ end
78
+
79
+ context "less than" do
80
+ should 'parse [op1]<[op2]' do
81
+ result = @parser.parse("size<12")
82
+ assert_equal( { :lt => { :left => "size", :right => { :number => "12" } } }, result)
83
+ end
84
+ should 'parse [op1] <[op2]' do
85
+ result = @parser.parse("size <12")
86
+ assert_equal( { :lt => { :left => "size", :right => { :number => "12" } } }, result)
87
+ end
88
+ should 'parse [op1]< [op2]' do
89
+ result = @parser.parse("size< 12")
90
+ assert_equal( { :lt => { :left => "size", :right => { :number => "12" } } }, result)
91
+ end
92
+ should 'parse [op1] < [op2]' do
93
+ result = @parser.parse("size < 12")
94
+ assert_equal( { :lt => { :left => "size", :right => { :number => "12" } } }, result)
95
+ end
96
+ end
97
+
98
+ context "less than or equal to" do
99
+ should 'parse [op1]<=[op2]' do
100
+ result = @parser.parse("size<=12")
101
+ assert_equal( { :lteq => { :left => "size", :right => { :number => '12' } } }, result)
102
+ end
103
+ should 'parse [op1] <=[op2]' do
104
+ result = @parser.parse("size <=12")
105
+ assert_equal( { :lteq => { :left => "size", :right => { :number => '12' } } }, result)
106
+ end
107
+ should 'parse [op1]<= [op2]' do
108
+ result = @parser.parse("size<= 12")
109
+ assert_equal( { :lteq => { :left => "size", :right => { :number => '12' } } }, result)
110
+ end
111
+ should 'parse [op1] <= [op2]' do
112
+ result = @parser.parse("size <= 12")
113
+ assert_equal( { :lteq => { :left => "size", :right => { :number => '12' } } }, result)
114
+ end
115
+ end
116
+
117
+ context "greater than" do
118
+ should 'parse [op1]>[op2]' do
119
+ result = @parser.parse("size>12")
120
+ assert_equal( { :gt => { :left => "size", :right => { :number => '12' } } }, result)
121
+ end
122
+ should 'parse [op1] >[op2]' do
123
+ result = @parser.parse("size >12")
124
+ assert_equal( { :gt => { :left => "size", :right => { :number => '12' } } }, result)
125
+ end
126
+ should 'parse [op1]> [op2]' do
127
+ result = @parser.parse("size> 12")
128
+ assert_equal( { :gt => { :left => "size", :right => { :number => '12' } } }, result)
129
+ end
130
+ should 'parse [op1] > [op2]' do
131
+ result = @parser.parse("size > 12")
132
+ assert_equal( { :gt => { :left => "size", :right => { :number => '12' } } }, result)
133
+ end
134
+ end
135
+
136
+ context "greater than or equal to" do
137
+ should 'parse [op1]>=[op2]' do
138
+ result = @parser.parse("size>=12")
139
+ assert_equal( { :gteq => { :left => "size", :right => { :number => '12' } } }, result)
140
+ end
141
+ should 'parse [op1] >=[op2]' do
142
+ result = @parser.parse("size >=12")
143
+ assert_equal( { :gteq => { :left => "size", :right => { :number => '12' } } }, result)
144
+ end
145
+ should 'parse [op1]>= [op2]' do
146
+ result = @parser.parse("size>= 12")
147
+ assert_equal( { :gteq => { :left => "size", :right => { :number => '12' } } }, result)
148
+ end
149
+ should 'parse [op1] >= [op2]' do
150
+ result = @parser.parse("size >= 12")
151
+ assert_equal( { :gteq => { :left => "size", :right => { :number => '12' } } }, result)
152
+ end
153
+ end
154
+ end
155
+
156
+ context 'operators' do
157
+ context 'and' do
158
+ should 'parse [comp]&&[comp]' do
159
+ result = @parser.parse("name=myles&&size>=12")
160
+ assert_equal( { :and => { :left => { :eq => { :left => 'name', :right => { :string => 'myles' } } }, :right => { :gteq => { :left => 'size', :right => { :number => '12' } } } } }, result)
161
+ end
162
+
163
+ should 'parse [comp] &&[comp]' do
164
+ result = @parser.parse("name=myles &&size>=12")
165
+ assert_equal( { :and => { :left => { :eq => { :left => 'name', :right => { :string => 'myles' } } }, :right => { :gteq => { :left => 'size', :right => { :number => '12' } } } } }, result)
166
+ end
167
+
168
+ should 'parse [comp]&& [comp]' do
169
+ result = @parser.parse("name=myles&& size>=12")
170
+ assert_equal( { :and => { :left => { :eq => { :left => 'name', :right => { :string => 'myles' } } }, :right => { :gteq => { :left => 'size', :right => { :number => '12' } } } } }, result)
171
+ end
172
+
173
+ should 'parse [comp] && [comp]' do
174
+ result = @parser.parse("name=myles && size>=12")
175
+ assert_equal( { :and => { :left => { :eq => { :left => 'name', :right => { :string => 'myles' } } }, :right => { :gteq => { :left => 'size', :right => { :number => '12' } } } } }, result)
176
+ end
177
+ end
178
+
179
+ context 'or' do
180
+ should 'parse [comp]||[comp]' do
181
+ result = @parser.parse("name=myles||size>=12")
182
+ assert_equal( { :or => { :left => { :eq => { :left => 'name', :right => { :string => 'myles' } } }, :right => { :gteq => { :left => 'size', :right => { :number => '12' } } } } }, result)
183
+ end
184
+
185
+ should 'parse [comp] ||[comp]' do
186
+ result = @parser.parse("name=myles ||size>=12")
187
+ assert_equal( { :or => { :left => { :eq => { :left => 'name', :right => { :string => 'myles' } } }, :right => { :gteq => { :left => 'size', :right => { :number => '12' } } } } }, result)
188
+ end
189
+
190
+ should 'parse [comp]|| [comp]' do
191
+ result = @parser.parse("name=myles|| size>=12")
192
+ assert_equal( { :or => { :left => { :eq => { :left => 'name', :right => { :string => 'myles' } } }, :right => { :gteq => { :left => 'size', :right => { :number => '12' } } } } }, result)
193
+ end
194
+
195
+ should 'parse [comp] || [comp]' do
196
+ result = @parser.parse("name=myles || size>=12")
197
+ assert_equal( { :or => { :left => { :eq => { :left => 'name', :right => { :string => 'myles' } } }, :right => { :gteq => { :left => 'size', :right => { :number => '12' } } } } }, result)
198
+ end
199
+ end
200
+ end
201
+
202
+ context 'group' do
203
+ should 'parse ([op] || [op])' do
204
+ result = @parser.parse("(name=myles || size>=12)")
205
+ assert_equal( { :or => { :left => { :eq => { :left => 'name', :right => { :string => 'myles' } } }, :right => { :gteq => { :left => 'size', :right => { :number => '12' } } } } }, result)
206
+ end
207
+
208
+ should 'parse [op] || ([op] && [op])' do
209
+ result = @parser.parse("name=myles || (job=programmer && size>=12)")
210
+ assert_equal( { :or => { :left => { :eq => { :left => 'name', :right => { :string => 'myles' } } }, :right => { :and => { :left => { :eq => { :left => 'job', :right => { :string => 'programmer' } } }, :right => { :gteq => { :left => 'size', :right => { :number => '12' } } } } } } }, result)
211
+ end
212
+ end
213
+ end
214
+ end
@@ -0,0 +1,102 @@
1
+ require 'helper'
2
+
3
+ class ProcessorTest < Test::Unit::TestCase
4
+ def setup_fixtures
5
+ Player.create!(:name => "Patrick 'Paddy' Carew", :position => 'lock', :first_cap => '1899-06-24', :active => false)
6
+ Player.create!(:name => "Charlie Ellis", :position => 'flanker', :first_cap => '1899-06-24', :active => false)
7
+ Player.create!(:name => "Arthur Corfe", :position => 'flanker', :first_cap => '1899-07-22', :active => false)
8
+ Player.create!(:name => "Syd Miller", :position => 'wing', :first_cap => '1899-08-05', :active => false)
9
+ Player.create!(:name => "Charlie Redwood", :position => 'wing', :first_cap => '1903-08-15', :active => false)
10
+ Player.create!(:name => "Patrick 'Pat' Walsh", :position => 'no. 8', :first_cap => '1904-07-02', :active => false)
11
+ Player.create!(:name => "John Manning", :position => 'fly-half', :first_cap => '1904-07-23', :active => false)
12
+ Player.create!(:name => "Dally Messenger", :position => 'wing', :first_cap => '1907-08-03', :active => false)
13
+ Player.create!(:name => "Salesi Ma'afu", :position => 'prop', :first_cap => '2010-06-05', :active => true)
14
+ Player.create!(:name => "James Slipper", :position => 'prop', :first_cap => nil, :active => true)
15
+ end
16
+
17
+ context 'Processor' do
18
+ setup do
19
+ ActiveRecord::Base.establish_connection({
20
+ :adapter => 'sqlite3',
21
+ :database => ':memory:',
22
+ :verbosity => 'quiet'
23
+ })
24
+ Arel::Table.engine = Arel::Sql::Engine.new(ActiveRecord::Base)
25
+ AddUsers.migrate(:up)
26
+ setup_fixtures
27
+ end
28
+
29
+ context 'Parsing' do
30
+ should 'construct simple boolean statements' do
31
+ player = Player.from_query('name = "Charlie Ellis"')
32
+ assert_equal 1, player.count
33
+ assert_equal 'Charlie Ellis', player.first.name
34
+ end
35
+
36
+ should 'construct two boolean statments' do
37
+ player = Player.from_query('position=lock && first_cap < 2010-01-01')
38
+ assert_equal 1, player.count
39
+ assert_equal "Patrick 'Paddy' Carew", player.first.name
40
+ end
41
+
42
+ should 'contruct nested brackets' do
43
+ player = Player.from_query("(position = wing || position = lock) && first_cap <= 1905-01-01").order('first_cap')
44
+ assert_equal 3, player.count
45
+ assert_equal "Patrick 'Paddy' Carew", player[0].name
46
+ assert_equal "Syd Miller", player[1].name
47
+ assert_equal "Charlie Redwood", player[2].name
48
+ end
49
+
50
+ should 'handle nulls' do
51
+ player = Player.from_query("first_cap = null")
52
+ assert_equal 1, player.count
53
+ assert_equal "James Slipper", player.first.name
54
+ end
55
+
56
+ should 'handle booleans' do
57
+ player = Player.from_query("active = true")
58
+ assert_equal 2, player.count
59
+ player = Player.from_query("active = false")
60
+ assert_equal 8, player.count
61
+ end
62
+
63
+ should 'handle dates' do
64
+ player = Player.from_query("first_cap > 2000-01-01")
65
+ assert_equal 1, player.count
66
+ assert_equal "Salesi Ma'afu", player.first.name
67
+ end
68
+
69
+ should 'process =' do
70
+ player = Player.from_query('name = "Charlie Ellis"')
71
+ assert_equal 1, player.count
72
+ assert_equal 'Charlie Ellis', player.first.name
73
+ end
74
+
75
+ should 'process !=' do
76
+ player = Player.from_query('name != "Charlie Ellis"')
77
+ assert_equal 9, player.count
78
+ assert_equal false, player.map(&:name).include?('Charlie Ellis')
79
+ end
80
+
81
+ should 'process >' do
82
+ player = Player.from_query('first_cap > 1907-08-03')
83
+ assert_equal 1, player.count
84
+ end
85
+
86
+ should 'process >=' do
87
+ player = Player.from_query('first_cap >= 1907-08-03')
88
+ assert_equal 2, player.count
89
+ end
90
+
91
+ should 'process <' do
92
+ player = Player.from_query('first_cap < 1907-08-03')
93
+ assert_equal 7, player.count
94
+ end
95
+
96
+ should 'process <=' do
97
+ player = Player.from_query('first_cap <= 1907-08-03')
98
+ assert_equal 8, player.count
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,13 @@
1
+ require 'helper'
2
+
3
+ class RailtieTest < Test::Unit::TestCase
4
+ context 'Railtie' do
5
+ should 'not enable from_query if has_query_parsing is not called' do
6
+ assert_equal false, Team.respond_to?(:from_query)
7
+ end
8
+
9
+ should 'enable from_query if has_query_parsing is called' do
10
+ assert_equal true, Player.respond_to?(:from_query)
11
+ end
12
+ end
13
+ end
data/wherewolf.gemspec ADDED
@@ -0,0 +1,88 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = "wherewolf"
8
+ s.version = "0.1.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Myles Eftos"]
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"
14
+ s.email = "myles@madpilot.com.au"
15
+ s.extra_rdoc_files = [
16
+ "LICENSE.txt",
17
+ "README.rdoc"
18
+ ]
19
+ s.files = [
20
+ ".document",
21
+ "Gemfile",
22
+ "Gemfile.lock",
23
+ "Guardfile",
24
+ "LICENSE.txt",
25
+ "README.rdoc",
26
+ "Rakefile",
27
+ "lib/wherewolf.rb",
28
+ "lib/wherewolf/parser.rb",
29
+ "lib/wherewolf/processor.rb",
30
+ "lib/wherewolf/railtie.rb",
31
+ "test/helper.rb",
32
+ "test/parser_test.rb",
33
+ "test/processor_test.rb",
34
+ "test/railtie_test.rb",
35
+ "wherewolf.gemspec"
36
+ ]
37
+ s.homepage = "http://github.com/madpilot/wherewolf"
38
+ s.licenses = ["MIT"]
39
+ s.require_paths = ["lib"]
40
+ s.rubygems_version = "1.8.15"
41
+ s.summary = "Query parser that converts search terms to AREL for use in APIs"
42
+
43
+ if s.respond_to? :specification_version then
44
+ s.specification_version = 3
45
+
46
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
47
+ s.add_runtime_dependency(%q<arel>, [">= 0"])
48
+ s.add_runtime_dependency(%q<parslet>, [">= 0"])
49
+ s.add_development_dependency(%q<shoulda>, [">= 0"])
50
+ s.add_development_dependency(%q<rdoc>, [">= 0"])
51
+ s.add_development_dependency(%q<bundler>, [">= 0"])
52
+ s.add_development_dependency(%q<jeweler>, [">= 0"])
53
+ s.add_development_dependency(%q<guard>, [">= 0"])
54
+ s.add_development_dependency(%q<guard-test>, [">= 0"])
55
+ s.add_development_dependency(%q<simplecov>, [">= 0"])
56
+ s.add_development_dependency(%q<sqlite3>, [">= 0"])
57
+ s.add_development_dependency(%q<rails>, [">= 0"])
58
+ s.add_development_dependency(%q<wherewolf>, [">= 0"])
59
+ else
60
+ s.add_dependency(%q<arel>, [">= 0"])
61
+ s.add_dependency(%q<parslet>, [">= 0"])
62
+ s.add_dependency(%q<shoulda>, [">= 0"])
63
+ s.add_dependency(%q<rdoc>, [">= 0"])
64
+ s.add_dependency(%q<bundler>, [">= 0"])
65
+ s.add_dependency(%q<jeweler>, [">= 0"])
66
+ s.add_dependency(%q<guard>, [">= 0"])
67
+ s.add_dependency(%q<guard-test>, [">= 0"])
68
+ s.add_dependency(%q<simplecov>, [">= 0"])
69
+ s.add_dependency(%q<sqlite3>, [">= 0"])
70
+ s.add_dependency(%q<rails>, [">= 0"])
71
+ s.add_dependency(%q<wherewolf>, [">= 0"])
72
+ end
73
+ else
74
+ s.add_dependency(%q<arel>, [">= 0"])
75
+ s.add_dependency(%q<parslet>, [">= 0"])
76
+ s.add_dependency(%q<shoulda>, [">= 0"])
77
+ s.add_dependency(%q<rdoc>, [">= 0"])
78
+ s.add_dependency(%q<bundler>, [">= 0"])
79
+ s.add_dependency(%q<jeweler>, [">= 0"])
80
+ s.add_dependency(%q<guard>, [">= 0"])
81
+ s.add_dependency(%q<guard-test>, [">= 0"])
82
+ s.add_dependency(%q<simplecov>, [">= 0"])
83
+ s.add_dependency(%q<sqlite3>, [">= 0"])
84
+ s.add_dependency(%q<rails>, [">= 0"])
85
+ s.add_dependency(%q<wherewolf>, [">= 0"])
86
+ end
87
+ end
88
+
metadata ADDED
@@ -0,0 +1,200 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: wherewolf
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Myles Eftos
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-10-10 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: arel
16
+ requirement: &70230035647040 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *70230035647040
25
+ - !ruby/object:Gem::Dependency
26
+ name: parslet
27
+ requirement: &70230035645880 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: *70230035645880
36
+ - !ruby/object:Gem::Dependency
37
+ name: shoulda
38
+ requirement: &70230035645020 !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: *70230035645020
47
+ - !ruby/object:Gem::Dependency
48
+ name: rdoc
49
+ requirement: &70230035658520 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ type: :development
56
+ prerelease: false
57
+ version_requirements: *70230035658520
58
+ - !ruby/object:Gem::Dependency
59
+ name: bundler
60
+ requirement: &70230035655100 !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ! '>='
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ type: :development
67
+ prerelease: false
68
+ version_requirements: *70230035655100
69
+ - !ruby/object:Gem::Dependency
70
+ name: jeweler
71
+ requirement: &70230035654260 !ruby/object:Gem::Requirement
72
+ none: false
73
+ requirements:
74
+ - - ! '>='
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: *70230035654260
80
+ - !ruby/object:Gem::Dependency
81
+ name: guard
82
+ requirement: &70230035653240 !ruby/object:Gem::Requirement
83
+ none: false
84
+ requirements:
85
+ - - ! '>='
86
+ - !ruby/object:Gem::Version
87
+ version: '0'
88
+ type: :development
89
+ prerelease: false
90
+ version_requirements: *70230035653240
91
+ - !ruby/object:Gem::Dependency
92
+ name: guard-test
93
+ requirement: &70230035681240 !ruby/object:Gem::Requirement
94
+ none: false
95
+ requirements:
96
+ - - ! '>='
97
+ - !ruby/object:Gem::Version
98
+ version: '0'
99
+ type: :development
100
+ prerelease: false
101
+ version_requirements: *70230035681240
102
+ - !ruby/object:Gem::Dependency
103
+ name: simplecov
104
+ requirement: &70230035677480 !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ! '>='
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ type: :development
111
+ prerelease: false
112
+ version_requirements: *70230035677480
113
+ - !ruby/object:Gem::Dependency
114
+ name: sqlite3
115
+ requirement: &70230035689500 !ruby/object:Gem::Requirement
116
+ none: false
117
+ requirements:
118
+ - - ! '>='
119
+ - !ruby/object:Gem::Version
120
+ version: '0'
121
+ type: :development
122
+ prerelease: false
123
+ version_requirements: *70230035689500
124
+ - !ruby/object:Gem::Dependency
125
+ name: rails
126
+ requirement: &70230035685280 !ruby/object:Gem::Requirement
127
+ none: false
128
+ requirements:
129
+ - - ! '>='
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: *70230035685280
135
+ - !ruby/object:Gem::Dependency
136
+ name: wherewolf
137
+ requirement: &70230035683060 !ruby/object:Gem::Requirement
138
+ none: false
139
+ requirements:
140
+ - - ! '>='
141
+ - !ruby/object:Gem::Version
142
+ version: '0'
143
+ type: :development
144
+ prerelease: false
145
+ version_requirements: *70230035683060
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
149
+ email: myles@madpilot.com.au
150
+ executables: []
151
+ extensions: []
152
+ extra_rdoc_files:
153
+ - LICENSE.txt
154
+ - README.rdoc
155
+ files:
156
+ - .document
157
+ - Gemfile
158
+ - Gemfile.lock
159
+ - Guardfile
160
+ - LICENSE.txt
161
+ - README.rdoc
162
+ - Rakefile
163
+ - lib/wherewolf.rb
164
+ - lib/wherewolf/parser.rb
165
+ - lib/wherewolf/processor.rb
166
+ - lib/wherewolf/railtie.rb
167
+ - test/helper.rb
168
+ - test/parser_test.rb
169
+ - test/processor_test.rb
170
+ - test/railtie_test.rb
171
+ - wherewolf.gemspec
172
+ homepage: http://github.com/madpilot/wherewolf
173
+ licenses:
174
+ - MIT
175
+ post_install_message:
176
+ rdoc_options: []
177
+ require_paths:
178
+ - lib
179
+ required_ruby_version: !ruby/object:Gem::Requirement
180
+ none: false
181
+ requirements:
182
+ - - ! '>='
183
+ - !ruby/object:Gem::Version
184
+ version: '0'
185
+ segments:
186
+ - 0
187
+ hash: 3955971048555964075
188
+ required_rubygems_version: !ruby/object:Gem::Requirement
189
+ none: false
190
+ requirements:
191
+ - - ! '>='
192
+ - !ruby/object:Gem::Version
193
+ version: '0'
194
+ requirements: []
195
+ rubyforge_project:
196
+ rubygems_version: 1.8.15
197
+ signing_key:
198
+ specification_version: 3
199
+ summary: Query parser that converts search terms to AREL for use in APIs
200
+ test_files: []