restful_query-rails3 0.4
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +5 -0
- data/History.txt +29 -0
- data/LICENSE +20 -0
- data/README.rdoc +64 -0
- data/Rakefile +30 -0
- data/init.rb +1 -0
- data/lib/restful_query/can_query.rb +38 -0
- data/lib/restful_query/condition.rb +110 -0
- data/lib/restful_query/parser.rb +166 -0
- data/lib/restful_query/sort.rb +72 -0
- data/lib/restful_query.rb +23 -0
- data/lib/sequel/extensions/restful_query.rb +25 -0
- data/rails/init.rb +3 -0
- data/restful_query-rails3.gemspec +72 -0
- data/tasks/restful_query_tasks.rake +4 -0
- data/test/test_helper.rb +35 -0
- data/test/test_restful_query_can_query.rb +20 -0
- data/test/test_restful_query_condition.rb +184 -0
- data/test/test_restful_query_parser.rb +467 -0
- data/test/test_restful_query_sort.rb +72 -0
- metadata +137 -0
data/History.txt
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
== 0.3.1 2010-02-16
|
2
|
+
|
3
|
+
* Parser takes a :map_columns option which maps aliases column names
|
4
|
+
* The parser now strips all conditions with blank values. If you actually want
|
5
|
+
to compare on a blank value, use the convertable value ':blank'
|
6
|
+
|
7
|
+
== 0.3.0 2009-08-10
|
8
|
+
|
9
|
+
* Vast improvements/enhancements
|
10
|
+
* Added Sequel::Dataset extension: restful_query - works like the can_query named_scope
|
11
|
+
* Added IN and NOT IN to Condition operators - value can be split on delimiter option
|
12
|
+
* Changed the default behavior to storing conditions as an array of condition hashes
|
13
|
+
* Added ability to pass an array of :conditions to the parser, see tests for format.
|
14
|
+
* Added English/Human names to condition operators
|
15
|
+
* Added list of CONVERTABLE_VALUES which are prefixed w/ : and are converted to their true ruby counterparts (nil, null, true, etc)
|
16
|
+
* Added operators for IS and IS NOT
|
17
|
+
* Added neq/!= operator to conditions
|
18
|
+
* Added a clear_default_sort! action that deletes the default before switching
|
19
|
+
|
20
|
+
== 0.2.0 2009-03-20
|
21
|
+
|
22
|
+
* 1 major enhancement:
|
23
|
+
* Now includes interface for sorting.
|
24
|
+
* Better documentation on the way.
|
25
|
+
|
26
|
+
== 0.0.1 2009-01-11
|
27
|
+
|
28
|
+
* 1 major enhancement:
|
29
|
+
* Initial release
|
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Aaron Quint, Quirkey NYC, LLC
|
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 NONINFRINGEMENT.
|
17
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
18
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
19
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
20
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
= restful_query
|
2
|
+
|
3
|
+
http://github.com/quirkey/restful_query
|
4
|
+
|
5
|
+
== DESCRIPTION:
|
6
|
+
|
7
|
+
RestfulQuery provides a RESTful interface for easily and safely querying ActiveRecord data.
|
8
|
+
|
9
|
+
== USAGE:
|
10
|
+
|
11
|
+
=== Rails/ActiveRecord:
|
12
|
+
|
13
|
+
# as a gem, in environment.rb
|
14
|
+
config.gem 'restful_query'
|
15
|
+
|
16
|
+
or install the plugin.
|
17
|
+
In your model:
|
18
|
+
|
19
|
+
class Post < ActiveRecord::Base
|
20
|
+
can_query
|
21
|
+
end
|
22
|
+
|
23
|
+
In your controller:
|
24
|
+
|
25
|
+
class PostsController < ApplicationController
|
26
|
+
# ...
|
27
|
+
def index
|
28
|
+
@posts = Post.restful_query(params[:query])
|
29
|
+
end
|
30
|
+
# ...
|
31
|
+
end
|
32
|
+
|
33
|
+
Now you can query your model via the URL:
|
34
|
+
|
35
|
+
/posts?query[name][like]=jon&query[_sort]=created_at-desc
|
36
|
+
|
37
|
+
=== Sequel
|
38
|
+
|
39
|
+
Theres also a Sequel extension that works very similar to ActiveRecord::Base.can_query:
|
40
|
+
|
41
|
+
require 'sequel/extensions/restful_query'
|
42
|
+
|
43
|
+
DB[:posts].restful_query({'name' => {'like' => 'jon'}, '_sort' => 'created_at-desc'})
|
44
|
+
<Sequel::SQLite::Dataset: "SELECT * FROM `posts` WHERE (name LIKE %jon%) ORDER BY `created_at` DESC">
|
45
|
+
|
46
|
+
=== More!
|
47
|
+
|
48
|
+
Please see the project homepage for detailed usage and info:
|
49
|
+
|
50
|
+
http://code.quirkey.com/restful_query
|
51
|
+
|
52
|
+
== INSTALL:
|
53
|
+
|
54
|
+
To install as a gem:
|
55
|
+
|
56
|
+
sudo gem install restful_query
|
57
|
+
|
58
|
+
To install as a rails plugin
|
59
|
+
|
60
|
+
./script/plugin install git://github.com/quirkey/restful_query.git
|
61
|
+
|
62
|
+
== LICENSE:
|
63
|
+
|
64
|
+
Free for use under the terms of the MIT License - see LICENSE for details
|
data/Rakefile
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
%w[rubygems rake rake/clean rake/testtask fileutils].each { |f| require f }
|
2
|
+
require File.dirname(__FILE__) + '/lib/restful_query'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |s|
|
7
|
+
s.name = %q{restful_query}
|
8
|
+
s.version = RestfulQuery::VERSION
|
9
|
+
s.authors = ["Aaron Quint"]
|
10
|
+
s.summary = 'Simple ActiveRecord and Sequel queries from a RESTful and safe interface'
|
11
|
+
s.description = %q{RestfulQuery provides a simple interface in front of a complex parser to parse specially formatted query hashes into complex SQL queries. It includes ActiveRecord and Sequel extensions.}
|
12
|
+
s.rubyforge_project = %q{quirkey}
|
13
|
+
s.add_runtime_dependency(%q<activesupport>, [">= 2.2.0"])
|
14
|
+
s.add_runtime_dependency(%q<chronic>, [">= 0.2.3"])
|
15
|
+
s.add_development_dependency(%q<Shoulda>, [">= 1.2.0"])
|
16
|
+
end
|
17
|
+
Jeweler::GemcutterTasks.new
|
18
|
+
rescue LoadError
|
19
|
+
puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
|
20
|
+
end
|
21
|
+
|
22
|
+
Rake::TestTask.new do |t|
|
23
|
+
t.libs << "test"
|
24
|
+
t.test_files = FileList['test/test*.rb']
|
25
|
+
t.verbose = true
|
26
|
+
end
|
27
|
+
|
28
|
+
Dir['tasks/**/*.rake'].each { |t| load t }
|
29
|
+
|
30
|
+
task :default => :test
|
data/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require File.join(File.expand_path(File.dirname(__FILE__)), 'rails', 'init.rb')
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module RestfulQuery
|
2
|
+
module CanQuery
|
3
|
+
|
4
|
+
def self.included(klass)
|
5
|
+
klass.extend MacroMethods
|
6
|
+
end
|
7
|
+
|
8
|
+
module MacroMethods
|
9
|
+
def can_query(options = {})
|
10
|
+
@include = options.delete(:include) || []
|
11
|
+
@query_options = options
|
12
|
+
@can_query = true
|
13
|
+
module_eval do
|
14
|
+
def self.restful_query_parser(query_hash, options = {})
|
15
|
+
RestfulQuery::Parser.new(query_hash, @query_options.merge(options))
|
16
|
+
end
|
17
|
+
|
18
|
+
scope_meth = self.respond_to?(:named_scope) ? :named_scope : :scope
|
19
|
+
|
20
|
+
send(scope_meth, :restful_query, lambda {|query_hash|
|
21
|
+
parser = self.restful_query_parser(query_hash)
|
22
|
+
query_hash = {}
|
23
|
+
query_hash[:conditions] = parser.to_conditions_array if parser.has_conditions?
|
24
|
+
query_hash[:include] = @include if @include && !@include.empty?
|
25
|
+
query_hash[:order] = parser.sort_sql if parser.has_sort?
|
26
|
+
logger.info 'Rest query:' + query_hash.inspect
|
27
|
+
query_hash
|
28
|
+
})
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def can_query?
|
33
|
+
@can_query
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
module RestfulQuery
|
2
|
+
class InvalidOperator < Error; end;
|
3
|
+
|
4
|
+
class Condition
|
5
|
+
attr_reader :column, :value, :operator, :options
|
6
|
+
|
7
|
+
OPERATOR_MAPPING = {
|
8
|
+
'lt' => '<',
|
9
|
+
'gt' => '>',
|
10
|
+
'gteq' => '>=',
|
11
|
+
'lteq' => '<=',
|
12
|
+
'eq' => '=',
|
13
|
+
'neq' => '!=',
|
14
|
+
'is' => 'IS',
|
15
|
+
'not' => 'IS NOT',
|
16
|
+
'like' => 'LIKE',
|
17
|
+
'in' => 'IN',
|
18
|
+
'notin' => 'NOT IN'
|
19
|
+
}.freeze
|
20
|
+
|
21
|
+
CONVERTABLE_VALUES = {
|
22
|
+
':true' => true,
|
23
|
+
':false' => false,
|
24
|
+
':nil' => nil,
|
25
|
+
':null' => nil,
|
26
|
+
':blank' => '',
|
27
|
+
':empty' => ''
|
28
|
+
}.freeze
|
29
|
+
|
30
|
+
ENGLISH_OPERATOR_MAPPING = {
|
31
|
+
'Less than' => 'lt',
|
32
|
+
'Greater than' => 'gt',
|
33
|
+
'Less than or equal to' => 'lteq',
|
34
|
+
'Greater than or equal to' => 'gteq',
|
35
|
+
'Equal to' => 'eq',
|
36
|
+
'Not equal to' => 'neq',
|
37
|
+
'Is' => 'is',
|
38
|
+
'Is not' => 'not',
|
39
|
+
'Like' => 'like',
|
40
|
+
'In' => 'in',
|
41
|
+
'Not in' => 'notin'
|
42
|
+
}.freeze
|
43
|
+
|
44
|
+
|
45
|
+
def initialize(column, value, operator = '=', options = {})
|
46
|
+
@options = {}
|
47
|
+
self.options = options if options.is_a?(Hash)
|
48
|
+
self.column = column
|
49
|
+
self.operator = operator
|
50
|
+
self.value = value
|
51
|
+
end
|
52
|
+
|
53
|
+
def map_operator(operator_to_look_up, reverse = false)
|
54
|
+
mapping = reverse ? OPERATOR_MAPPING.dup.invert : OPERATOR_MAPPING.dup
|
55
|
+
return operator_to_look_up if mapping.values.include?(operator_to_look_up)
|
56
|
+
found = mapping[operator_to_look_up.to_s]
|
57
|
+
end
|
58
|
+
|
59
|
+
def operator=(operator)
|
60
|
+
@operator = map_operator(operator)
|
61
|
+
raise(RestfulQuery::InvalidOperator, "#{@operator} is not a valid operator") unless @operator
|
62
|
+
end
|
63
|
+
|
64
|
+
def column=(column)
|
65
|
+
@column = column.to_s
|
66
|
+
end
|
67
|
+
|
68
|
+
def value=(value)
|
69
|
+
@value = parse_value(value)
|
70
|
+
end
|
71
|
+
|
72
|
+
def options=(options)
|
73
|
+
options.each {|k, v| @options[k.to_sym] = v }
|
74
|
+
end
|
75
|
+
|
76
|
+
def to_hash
|
77
|
+
{column => {map_operator(operator, true) => value}}
|
78
|
+
end
|
79
|
+
|
80
|
+
def to_condition_array
|
81
|
+
["#{column} #{operator} #{placeholder}", value]
|
82
|
+
end
|
83
|
+
|
84
|
+
def placeholder
|
85
|
+
if ['IN', 'NOT IN'].include?(operator)
|
86
|
+
'(?)'
|
87
|
+
else
|
88
|
+
'?'
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
protected
|
93
|
+
def parse_value(value)
|
94
|
+
if operator == 'LIKE'
|
95
|
+
"%#{value}%"
|
96
|
+
elsif ['IN', 'NOT IN'].include?(operator) && !value.is_a?(Array)
|
97
|
+
value.split(options[:delimiter] || ',')
|
98
|
+
elsif options[:integer]
|
99
|
+
value.to_i
|
100
|
+
elsif options[:chronic]
|
101
|
+
Chronic.parse(value.to_s)
|
102
|
+
elsif value =~ /^\:/ && CONVERTABLE_VALUES.has_key?(value)
|
103
|
+
CONVERTABLE_VALUES[value]
|
104
|
+
else
|
105
|
+
value
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,166 @@
|
|
1
|
+
module RestfulQuery
|
2
|
+
class Parser
|
3
|
+
attr_reader :query, :exclude_columns, :map_columns, :integer_columns, :options
|
4
|
+
|
5
|
+
def initialize(query, options = {})
|
6
|
+
@options = options || {}
|
7
|
+
@exclude_columns = columns_from_options(:exclude, options)
|
8
|
+
@integer_columns = columns_from_options(:integer, options)
|
9
|
+
@map_columns = options[:map_columns] || {}
|
10
|
+
@default_sort = options[:default_sort] ? [Sort.parse(options[:default_sort])] : []
|
11
|
+
@single_sort = options[:single_sort] || false
|
12
|
+
@query = (query || {}).dup
|
13
|
+
@default_join = @query.delete(:join) || :and
|
14
|
+
extract_sorts_from_conditions
|
15
|
+
map_conditions
|
16
|
+
end
|
17
|
+
|
18
|
+
def conditions
|
19
|
+
conditions_hash.values.flatten
|
20
|
+
end
|
21
|
+
|
22
|
+
def has_conditions?
|
23
|
+
!conditions.empty?
|
24
|
+
end
|
25
|
+
|
26
|
+
def conditions_for(column)
|
27
|
+
conditions_hash[column.to_s]
|
28
|
+
end
|
29
|
+
|
30
|
+
def to_conditions_array(join = nil)
|
31
|
+
join ||= @default_join
|
32
|
+
join_string = (join == :or) ? ' OR ' : ' AND '
|
33
|
+
conditions_string = []
|
34
|
+
conditions_values = []
|
35
|
+
conditions.each do |c|
|
36
|
+
ca = c.to_condition_array
|
37
|
+
conditions_string << ca[0]
|
38
|
+
conditions_values << ca[1]
|
39
|
+
end
|
40
|
+
conditions_values.unshift(conditions_string.join(join_string))
|
41
|
+
end
|
42
|
+
|
43
|
+
def to_query_hash
|
44
|
+
hash = @query
|
45
|
+
hash['_sort'] = sorts.collect {|s| s.to_s } unless sorts.empty?
|
46
|
+
hash
|
47
|
+
end
|
48
|
+
|
49
|
+
def sort_sql
|
50
|
+
@sorts.collect {|s| s.to_sql }.join(', ')
|
51
|
+
end
|
52
|
+
|
53
|
+
def has_sort?
|
54
|
+
!sorts.empty?
|
55
|
+
end
|
56
|
+
|
57
|
+
def sorts
|
58
|
+
@sorts ||= []
|
59
|
+
end
|
60
|
+
|
61
|
+
def sorted_columns
|
62
|
+
sorts.collect {|s| s.column }
|
63
|
+
end
|
64
|
+
|
65
|
+
def sorted_by?(column_name)
|
66
|
+
column = map_column(column_name)
|
67
|
+
sorted_columns.include?(column.to_s)
|
68
|
+
end
|
69
|
+
|
70
|
+
def sort(column_name)
|
71
|
+
column = map_column(column_name)
|
72
|
+
sorts.detect {|s| s && s.column == column }
|
73
|
+
end
|
74
|
+
|
75
|
+
def set_sort(column_name, direction)
|
76
|
+
column = map_column(column_name)
|
77
|
+
if new_sort = self.sort(column_name)
|
78
|
+
if direction.nil?
|
79
|
+
self.sorts.reject! {|s| s.column == column.to_s }
|
80
|
+
else
|
81
|
+
new_sort.direction = direction
|
82
|
+
end
|
83
|
+
else
|
84
|
+
new_sort = Sort.new(column, direction)
|
85
|
+
self.sorts << new_sort
|
86
|
+
end
|
87
|
+
new_sort
|
88
|
+
end
|
89
|
+
|
90
|
+
def clear_default_sort!
|
91
|
+
@sorts.reject! {|s| s == @default_sort.first }
|
92
|
+
end
|
93
|
+
|
94
|
+
protected
|
95
|
+
def add_condition_for(column, condition)
|
96
|
+
conditions_hash[column.to_s] ||= []
|
97
|
+
conditions_hash[column.to_s] << condition
|
98
|
+
end
|
99
|
+
|
100
|
+
def conditions_hash
|
101
|
+
@conditions_hash ||= {}
|
102
|
+
end
|
103
|
+
|
104
|
+
def chronic_columns
|
105
|
+
if chronic = options[:chronic]
|
106
|
+
chronic.is_a?(Array) ? chronic.collect {|c| c.to_s } : ['created_at', 'updated_at']
|
107
|
+
else
|
108
|
+
[]
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def extract_sorts_from_conditions
|
113
|
+
@sorts = sorts_from_hash(@query.delete('_sort'))
|
114
|
+
@sorts = @default_sort if @sorts.empty?
|
115
|
+
end
|
116
|
+
|
117
|
+
def map_conditions
|
118
|
+
conditions = query.delete(:conditions) || query
|
119
|
+
if conditions.is_a?(Hash)
|
120
|
+
conditions_array = []
|
121
|
+
conditions.each do |column, hash_conditions|
|
122
|
+
if hash_conditions.is_a?(Hash)
|
123
|
+
hash_conditions.each do |operator, value|
|
124
|
+
conditions_array << {'column' => column, 'operator' => operator, 'value' => value}
|
125
|
+
end
|
126
|
+
else
|
127
|
+
conditions_array << {'column' => column, 'value' => hash_conditions}
|
128
|
+
end
|
129
|
+
end
|
130
|
+
conditions = conditions_array
|
131
|
+
end
|
132
|
+
# with a normalized array of conditions
|
133
|
+
conditions.each do |condition|
|
134
|
+
column, operator, value = condition['column'], condition['operator'] || 'eq', condition['value']
|
135
|
+
unless exclude_columns.include?(column.to_s) || value == ''
|
136
|
+
condition_options = {}
|
137
|
+
condition_options[:chronic] = true if chronic_columns.include?(column.to_s)
|
138
|
+
condition_options[:integer] = true if integer_columns.include?(column.to_s)
|
139
|
+
condition_options.merge!(condition['options'] || {})
|
140
|
+
column_name = map_column(column)
|
141
|
+
add_condition_for(column, Condition.new(column_name, value, operator, condition_options))
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
def sorts_from_hash(sorts)
|
147
|
+
sort_conditions = [sorts].flatten.compact
|
148
|
+
sort_conditions.collect do |c|
|
149
|
+
s = Sort.parse(c)
|
150
|
+
s.column = map_column(s.column)
|
151
|
+
s
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
def columns_from_options(column_type, options)
|
156
|
+
option = "#{column_type}_columns".to_sym
|
157
|
+
options[option] ? [options.delete(option)].flatten.collect {|c| c.to_s } : []
|
158
|
+
end
|
159
|
+
|
160
|
+
def map_column(column_name)
|
161
|
+
map_columns[column_name.to_s] || column_name.to_s
|
162
|
+
end
|
163
|
+
|
164
|
+
|
165
|
+
end
|
166
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module RestfulQuery
|
2
|
+
class InvalidDirection < Error; end
|
3
|
+
|
4
|
+
class Sort
|
5
|
+
include Comparable
|
6
|
+
|
7
|
+
attr_reader :column, :direction
|
8
|
+
|
9
|
+
DIRECTIONS = {
|
10
|
+
'up' => 'ASC',
|
11
|
+
'asc' => 'ASC',
|
12
|
+
'ASC' => 'ASC',
|
13
|
+
'down' => 'DESC',
|
14
|
+
'desc' => 'DESC',
|
15
|
+
'DESC' => 'DESC'
|
16
|
+
}.freeze
|
17
|
+
|
18
|
+
|
19
|
+
def initialize(column, direction)
|
20
|
+
self.column = column
|
21
|
+
self.direction = direction
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.parse(sort_string, split_on = /\-|\ /)
|
25
|
+
return unless sort_string
|
26
|
+
column, direction = sort_string.split(split_on)
|
27
|
+
new(column, direction)
|
28
|
+
end
|
29
|
+
|
30
|
+
def column=(column)
|
31
|
+
@column = column.to_s
|
32
|
+
end
|
33
|
+
|
34
|
+
def direction=(direction)
|
35
|
+
@direction = DIRECTIONS[direction.to_s]
|
36
|
+
raise(InvalidDirection, "'#{direction}' is not a valid order direction") unless @direction
|
37
|
+
end
|
38
|
+
|
39
|
+
def reverse_direction
|
40
|
+
direction == 'ASC' ? 'DESC' : 'ASC'
|
41
|
+
end
|
42
|
+
|
43
|
+
def ==(other)
|
44
|
+
return false unless other.is_a?(Sort)
|
45
|
+
column == other.column && direction == other.direction
|
46
|
+
end
|
47
|
+
|
48
|
+
# Makes a roundabout for directions nil -> desc -> asc -> nil
|
49
|
+
def self.next_direction(current_direction)
|
50
|
+
case current_direction.to_s.downcase
|
51
|
+
when 'desc'
|
52
|
+
'asc'
|
53
|
+
when 'asc'
|
54
|
+
nil
|
55
|
+
else
|
56
|
+
'desc'
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def next_direction
|
61
|
+
self.class.next_direction(direction)
|
62
|
+
end
|
63
|
+
|
64
|
+
def to_s(join = '-')
|
65
|
+
"#{column}#{join}#{direction.downcase}"
|
66
|
+
end
|
67
|
+
|
68
|
+
def to_sql
|
69
|
+
"#{column} #{direction}"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
$:.unshift(File.dirname(__FILE__)) unless
|
2
|
+
$:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
|
3
|
+
|
4
|
+
require 'active_support'
|
5
|
+
begin
|
6
|
+
require 'chronic'
|
7
|
+
rescue LoadError
|
8
|
+
warn 'In order to use the time parsing functionalities you must install the Chronic gem: sudo gem install chronic'
|
9
|
+
end
|
10
|
+
|
11
|
+
module RestfulQuery
|
12
|
+
VERSION = '0.3.1'
|
13
|
+
|
14
|
+
class Error < RuntimeError; end
|
15
|
+
end
|
16
|
+
|
17
|
+
%w{condition sort parser can_query}.each do |lib|
|
18
|
+
require File.join("restful_query","#{lib}.rb")
|
19
|
+
end
|
20
|
+
|
21
|
+
if defined?(ActiveRecord)
|
22
|
+
ActiveRecord::Base.send(:include, RestfulQuery::CanQuery)
|
23
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), '..', '..', 'restful_query.rb')
|
2
|
+
|
3
|
+
module RestfulQuery
|
4
|
+
class Sort
|
5
|
+
|
6
|
+
def to_sequel
|
7
|
+
column.to_sym.send(direction.downcase)
|
8
|
+
end
|
9
|
+
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
module Sequel
|
14
|
+
class Dataset
|
15
|
+
|
16
|
+
def restful_query(query_hash, options = {})
|
17
|
+
parser = RestfulQuery::Parser.new(query_hash, options = {})
|
18
|
+
collection = self
|
19
|
+
collection = collection.filter(*parser.to_conditions_array) if parser.has_conditions?
|
20
|
+
collection = collection.order(*parser.sorts.collect {|s| s.to_sequel }) if parser.has_sort?
|
21
|
+
collection
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
data/rails/init.rb
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{restful_query-rails3}
|
8
|
+
s.version = "0.4"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Aaron Quint"]
|
12
|
+
s.date = %q{2011-02-16}
|
13
|
+
s.description = %q{Rails3 Version of RestfulQuery, provides a simple interface in front of a complex parser to parse specially formatted query hashes into complex SQL queries. It includes ActiveRecord and Sequel extensions.}
|
14
|
+
s.extra_rdoc_files = [
|
15
|
+
"LICENSE",
|
16
|
+
"README.rdoc"
|
17
|
+
]
|
18
|
+
s.files = [
|
19
|
+
".gitignore",
|
20
|
+
"History.txt",
|
21
|
+
"LICENSE",
|
22
|
+
"README.rdoc",
|
23
|
+
"Rakefile",
|
24
|
+
"init.rb",
|
25
|
+
"restful_query-rails3.gemspec",
|
26
|
+
"lib/restful_query.rb",
|
27
|
+
"lib/restful_query/can_query.rb",
|
28
|
+
"lib/restful_query/condition.rb",
|
29
|
+
"lib/restful_query/parser.rb",
|
30
|
+
"lib/restful_query/sort.rb",
|
31
|
+
"lib/sequel/extensions/restful_query.rb",
|
32
|
+
"rails/init.rb",
|
33
|
+
"tasks/restful_query_tasks.rake",
|
34
|
+
"test/test_helper.rb",
|
35
|
+
"test/test_restful_query_can_query.rb",
|
36
|
+
"test/test_restful_query_condition.rb",
|
37
|
+
"test/test_restful_query_parser.rb",
|
38
|
+
"test/test_restful_query_sort.rb"
|
39
|
+
]
|
40
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
41
|
+
s.require_paths = ["lib"]
|
42
|
+
s.rubyforge_project = %q{quirkey}
|
43
|
+
s.rubygems_version = %q{1.3.5}
|
44
|
+
s.summary = %q{Simple ActiveRecord and Sequel queries from a RESTful and safe interface}
|
45
|
+
s.test_files = [
|
46
|
+
"test/test_helper.rb",
|
47
|
+
"test/test_restful_query_can_query.rb",
|
48
|
+
"test/test_restful_query_condition.rb",
|
49
|
+
"test/test_restful_query_parser.rb",
|
50
|
+
"test/test_restful_query_sort.rb"
|
51
|
+
]
|
52
|
+
|
53
|
+
if s.respond_to? :specification_version then
|
54
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
55
|
+
s.specification_version = 3
|
56
|
+
|
57
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
58
|
+
s.add_runtime_dependency(%q<activesupport>, [">= 2.2.0"])
|
59
|
+
s.add_runtime_dependency(%q<chronic>, [">= 0.2.3"])
|
60
|
+
s.add_development_dependency(%q<Shoulda>, [">= 1.2.0"])
|
61
|
+
else
|
62
|
+
s.add_dependency(%q<activesupport>, [">= 2.2.0"])
|
63
|
+
s.add_dependency(%q<chronic>, [">= 0.2.3"])
|
64
|
+
s.add_dependency(%q<Shoulda>, [">= 1.2.0"])
|
65
|
+
end
|
66
|
+
else
|
67
|
+
s.add_dependency(%q<activesupport>, [">= 2.2.0"])
|
68
|
+
s.add_dependency(%q<chronic>, [">= 0.2.3"])
|
69
|
+
s.add_dependency(%q<Shoulda>, [">= 1.2.0"])
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|