restful_query-rails3 0.4

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ .DS_Store
2
+ tmp*
3
+ pkg*
4
+ .specification
5
+ _site*
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,3 @@
1
+ require File.join(File.expand_path(File.dirname(__FILE__)),'..','lib','restful_query.rb')
2
+
3
+ ActiveRecord::Base.send(:include, RestfulQuery::CanQuery)
@@ -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
+
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :restful_query do
3
+ # # Task goes here
4
+ # end