restful_query 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt ADDED
@@ -0,0 +1,10 @@
1
+ == 0.2.0 2009-03-20
2
+
3
+ * 1 major enhancement:
4
+ * Now includes interface for sorting.
5
+ * Better documentation on the way.
6
+
7
+ == 0.0.1 2009-01-11
8
+
9
+ * 1 major enhancement:
10
+ * 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/Manifest.txt ADDED
@@ -0,0 +1,19 @@
1
+ History.txt
2
+ LICENSE
3
+ Manifest.txt
4
+ README.rdoc
5
+ Rakefile
6
+ init.rb
7
+ lib/restful_query.rb
8
+ lib/restful_query/can_query.rb
9
+ lib/restful_query/condition.rb
10
+ lib/restful_query/parser.rb
11
+ lib/restful_query/sort.rb
12
+ rails/init.rb
13
+ restful_query.gemspec
14
+ tasks/restful_query_tasks.rake
15
+ test/test_helper.rb
16
+ test/test_restful_query_can_query.rb
17
+ test/test_restful_query_condition.rb
18
+ test/test_restful_query_parser.rb
19
+ test/test_restful_query_sort.rb
data/README.rdoc ADDED
@@ -0,0 +1,29 @@
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
+ Please see the project homepage for usage and more info:
12
+
13
+ http://code.quirkey.com/restful_query
14
+
15
+ == INSTALL:
16
+
17
+ To install as a gem:
18
+
19
+ sudo gem install restful_query
20
+
21
+ or from github:
22
+
23
+ sudo gem install quirkey-restful_query -s http://gems.github.com
24
+
25
+ To install as a rails plugin
26
+
27
+ == LICENSE:
28
+
29
+ Free for use under the terms of the MIT License - see LICENSE for details
data/Rakefile ADDED
@@ -0,0 +1,32 @@
1
+ %w[rubygems rake rake/clean fileutils newgem rubigen].each { |f| require f }
2
+ require File.dirname(__FILE__) + '/lib/restful_query'
3
+
4
+ # Generate all the Rake tasks
5
+ # Run 'rake -T' to see list of generated tasks (from gem root directory)
6
+ $hoe = Hoe.new('restful_query', RestfulQuery::VERSION) do |p|
7
+ p.developer('Aaron Quint', 'aaron@quirkey.com')
8
+ p.changes = p.paragraphs_of("History.txt", 0..1).join("\n\n")
9
+ p.rubyforge_name = 'quirkey'
10
+ p.description = p.summary = 'Simple ActiveRecord queries from a RESTful and safe interface'
11
+ p.url = 'http://code.quirkey.com/restful_query'
12
+ p.extra_deps = [
13
+ ['activesupport','>= 2.2.0'],
14
+ ['activerecord','>= 2.2.0'],
15
+ ['chronic','>= 0.2.3']
16
+ ]
17
+ p.extra_dev_deps = [
18
+ ['newgem', ">= #{::Newgem::VERSION}"],
19
+ ['Shoulda', '>= 1.2.0']
20
+ ]
21
+
22
+ p.clean_globs |= %w[**/.DS_Store tmp *.log]
23
+ path = (p.rubyforge_name == p.name) ? p.rubyforge_name : "\#{p.rubyforge_name}/\#{p.name}"
24
+ p.remote_rdoc_dir = File.join(path.gsub(/^#{p.rubyforge_name}\/?/,''), 'rdoc')
25
+ p.rsync_args = '-av --delete --ignore-errors'
26
+ end
27
+
28
+ require 'newgem/tasks' # load /tasks/*.rake
29
+ Dir['tasks/**/*.rake'].each { |t| load t }
30
+
31
+ # TODO - want other tests/tasks run by default? Add them to the list
32
+ # task :default => [:spec, :features]
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require File.join(File.dirname(__FILE__), 'rails', 'init.rb')
@@ -0,0 +1,36 @@
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
+ named_scope :restful_query, lambda {|query_hash|
19
+ parser = self.restful_query_parser(query_hash)
20
+ query_hash = {}
21
+ query_hash[:conditions] = parser.to_conditions_array if parser.has_conditions?
22
+ query_hash[:include] = @include if @include && !@include.empty?
23
+ query_hash[:order] = parser.sort_sql if parser.has_sort?
24
+ logger.info 'Rest query:' + query_hash.inspect
25
+ query_hash
26
+ }
27
+ end
28
+ end
29
+
30
+ def can_query?
31
+ @can_query
32
+ end
33
+ end
34
+
35
+ end
36
+ end
@@ -0,0 +1,77 @@
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
+ 'like' => 'LIKE'
14
+ }.freeze
15
+
16
+ REVERSE_OPERATOR_MAPPING = {
17
+ '<' => 'lt',
18
+ '>' => 'gt',
19
+ '>=' => 'gteq',
20
+ '<=' => 'lteq',
21
+ '=' => 'eq',
22
+ 'LIKE' => 'like'
23
+ }.freeze
24
+
25
+ def initialize(column, value, operator = '=', options = {})
26
+ @options = {}
27
+ @options = options if options.is_a?(Hash)
28
+ self.column = column
29
+ self.value = value
30
+ self.operator = operator
31
+ end
32
+
33
+ def map_operator(operator_to_look_up, reverse = false)
34
+ mapping = reverse ? REVERSE_OPERATOR_MAPPING.dup : OPERATOR_MAPPING.dup
35
+ return operator_to_look_up if mapping.values.include?(operator_to_look_up)
36
+ found = mapping[operator_to_look_up.to_s]
37
+ end
38
+
39
+ def operator=(operator)
40
+ @operator = map_operator(operator)
41
+ raise(RestfulQuery::InvalidOperator, "#{@operator} is not a valid operator") unless @operator
42
+ end
43
+
44
+ def column=(column)
45
+ @column = column.to_s
46
+ end
47
+
48
+ def value=(value)
49
+ @value = parse_value(value)
50
+ end
51
+
52
+ def to_hash
53
+ {column => {map_operator(operator, true) => value}}
54
+ end
55
+
56
+ def to_condition_array
57
+ parsed_value = if operator == 'LIKE'
58
+ "%#{value}%"
59
+ elsif options[:integer]
60
+ value.to_i
61
+ else
62
+ value
63
+ end
64
+ ["#{column} #{operator} ?", parsed_value]
65
+ end
66
+
67
+ protected
68
+ def parse_value(value)
69
+ if options[:chronic]
70
+ Chronic.parse(value.to_s)
71
+ else
72
+ value
73
+ end
74
+ end
75
+
76
+ end
77
+ end
@@ -0,0 +1,132 @@
1
+ module RestfulQuery
2
+ class Parser
3
+ attr_reader :query_hash, :exclude_columns, :integer_columns, :options
4
+
5
+ def initialize(query_hash, options = {})
6
+ @options = options || {}
7
+ @exclude_columns = options[:exclude_columns] ? [options.delete(:exclude_columns)].flatten.collect {|c| c.to_s } : []
8
+ @integer_columns = options[:integer_columns] ? [options.delete(:integer_columns)].flatten.collect {|c| c.to_s } : []
9
+ @default_sort = options[:default_sort] ? [Sort.parse(options[:default_sort])] : []
10
+ @query_hash = (query_hash || {}).dup
11
+ @default_join = @query_hash.delete(:join) || :and
12
+ extract_sorts_from_conditions
13
+ map_hash_to_conditions
14
+ end
15
+
16
+ def conditions
17
+ conditions_hash.values.flatten
18
+ end
19
+
20
+ def has_conditions?
21
+ !conditions.empty?
22
+ end
23
+
24
+ def conditions_for(column)
25
+ conditions_hash[column.to_s]
26
+ end
27
+
28
+ def to_conditions_array(join = nil)
29
+ join ||= @default_join
30
+ join_string = (join == :or) ? ' OR ' : ' AND '
31
+ conditions_string = []
32
+ conditions_values = []
33
+ conditions.each do |c|
34
+ ca = c.to_condition_array
35
+ conditions_string << ca[0]
36
+ conditions_values << ca[1]
37
+ end
38
+ conditions_values.unshift(conditions_string.join(join_string))
39
+ end
40
+
41
+ def to_query_hash
42
+ hash = @query_hash
43
+ hash['join'] = @default_join
44
+ hash['_sort'] = sorts.collect {|s| s.to_s }
45
+ hash
46
+ end
47
+
48
+ def self.sorts_from_hash(sorts)
49
+ sort_conditions = [sorts].flatten.compact
50
+ sort_conditions.collect {|c| Sort.parse(c) }
51
+ end
52
+
53
+ def sort_sql
54
+ @sorts.collect {|s| s.to_sql }.join(', ')
55
+ end
56
+
57
+ def has_sort?
58
+ !sorts.empty?
59
+ end
60
+
61
+ def sorts
62
+ @sorts ||= []
63
+ end
64
+
65
+ def sorted_columns
66
+ sorts.collect {|s| s.column }
67
+ end
68
+
69
+ def sorted_by?(column)
70
+ sorted_columns.include?(column.to_s)
71
+ end
72
+
73
+ def sort(column)
74
+ sorts.detect {|s| s && s.column == column.to_s }
75
+ end
76
+
77
+ def set_sort(column, direction)
78
+ if new_sort = self.sort(column)
79
+ if direction.nil?
80
+ self.sorts.reject! {|s| s.column == column.to_s }
81
+ else
82
+ new_sort.direction = direction
83
+ end
84
+ else
85
+ new_sort = Sort.new(column, direction)
86
+ self.sorts << new_sort
87
+ end
88
+ new_sort
89
+ end
90
+
91
+ protected
92
+ def add_condition_for(column, condition)
93
+ conditions_hash[column.to_s] ||= []
94
+ conditions_hash[column.to_s] << condition
95
+ end
96
+
97
+ def conditions_hash
98
+ @conditions_hash ||= {}
99
+ end
100
+
101
+ def chronic_columns
102
+ if chronic = options[:chronic]
103
+ chronic.is_a?(Array) ? chronic.collect {|c| c.to_s } : ['created_at', 'updated_at']
104
+ else
105
+ []
106
+ end
107
+ end
108
+
109
+ def extract_sorts_from_conditions
110
+ @sorts = self.class.sorts_from_hash(@query_hash.delete('_sort'))
111
+ @sorts = @default_sort if @sorts.empty?
112
+ end
113
+
114
+ def map_hash_to_conditions
115
+ @query_hash.each do |column, hash_conditions|
116
+ unless exclude_columns.include?(column.to_s)
117
+ condition_options = {}
118
+ condition_options[:chronic] = true if chronic_columns.include?(column.to_s)
119
+ condition_options[:integer] = true if integer_columns.include?(column.to_s)
120
+ if hash_conditions.is_a?(Hash)
121
+ hash_conditions.each do |operator, value|
122
+ add_condition_for(column, Condition.new(column, value, operator, condition_options))
123
+ end
124
+ else
125
+ add_condition_for(column, Condition.new(column, hash_conditions, '=', condition_options))
126
+ end
127
+ end
128
+ end
129
+ end
130
+
131
+ end
132
+ end
@@ -0,0 +1,65 @@
1
+ module RestfulQuery
2
+ class InvalidDirection < Error; end
3
+
4
+ class Sort
5
+ attr_reader :column, :direction
6
+
7
+ DIRECTIONS = {
8
+ 'up' => 'ASC',
9
+ 'asc' => 'ASC',
10
+ 'ASC' => 'ASC',
11
+ 'down' => 'DESC',
12
+ 'desc' => 'DESC',
13
+ 'DESC' => 'DESC'
14
+ }.freeze
15
+
16
+
17
+ def initialize(column, direction)
18
+ self.column = column
19
+ self.direction = direction
20
+ end
21
+
22
+ def self.parse(sort_string, split_on = /-|\ /)
23
+ return unless sort_string
24
+ column, direction = sort_string.split(split_on)
25
+ new(column, direction)
26
+ end
27
+
28
+ def column=(column)
29
+ @column = column.to_s
30
+ end
31
+
32
+ def direction=(direction)
33
+ @direction = DIRECTIONS[direction.to_s]
34
+ raise(InvalidDirection, "'#{direction}' is not a valid order direction") unless @direction
35
+ end
36
+
37
+ def reverse_direction
38
+ direction == 'ASC' ? 'DESC' : 'ASC'
39
+ end
40
+
41
+ # Makes a roundabout for directions nil -> desc -> asc -> nil
42
+ def self.next_direction(current_direction)
43
+ case current_direction.to_s.downcase
44
+ when 'desc'
45
+ 'asc'
46
+ when 'asc'
47
+ nil
48
+ else
49
+ 'desc'
50
+ end
51
+ end
52
+
53
+ def next_direction
54
+ self.class.next_direction(direction)
55
+ end
56
+
57
+ def to_s(join = '-')
58
+ "#{column}#{join}#{direction.downcase}"
59
+ end
60
+
61
+ def to_sql
62
+ "#{column} #{direction}"
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,22 @@
1
+ $:.unshift(File.dirname(__FILE__)) unless
2
+ $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
3
+
4
+ begin
5
+ require 'chronic'
6
+ unless defined?(ActiveSupport)
7
+ require 'active_support'
8
+ end
9
+ rescue LoadError
10
+ warn 'In order to use the time parsing functionalities you must install the Chronic gem: sudo gem install chronic'
11
+ end
12
+
13
+ module RestfulQuery
14
+ VERSION = '0.2.0'
15
+
16
+ class Error < RuntimeError; end
17
+ end
18
+
19
+
20
+ %w{condition sort parser can_query}.each do |lib|
21
+ require File.join("restful_query","#{lib}.rb")
22
+ end
data/rails/init.rb ADDED
@@ -0,0 +1,3 @@
1
+ require File.join(File.dirname(__FILE__),'..','lib','restful_query.rb')
2
+
3
+ ActiveRecord::Base.send(:include, RestfulQuery::CanQuery)
@@ -0,0 +1,50 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = %q{restful_query}
5
+ s.version = "0.2.0"
6
+
7
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
8
+ s.authors = ["Aaron Quint"]
9
+ s.date = %q{2009-03-21}
10
+ s.description = %q{Simple ActiveRecord queries from a RESTful and safe interface}
11
+ s.email = ["aaron@quirkey.com"]
12
+ s.extra_rdoc_files = ["History.txt", "Manifest.txt", "README.rdoc"]
13
+ s.files = ["History.txt", "LICENSE", "Manifest.txt", "README.rdoc", "Rakefile", "init.rb", "lib/restful_query.rb", "lib/restful_query/can_query.rb", "lib/restful_query/condition.rb", "lib/restful_query/parser.rb", "lib/restful_query/sort.rb", "rails/init.rb", "restful_query.gemspec", "tasks/restful_query_tasks.rake", "test/test_helper.rb", "test/test_restful_query_can_query.rb", "test/test_restful_query_condition.rb", "test/test_restful_query_parser.rb", "test/test_restful_query_sort.rb"]
14
+ s.has_rdoc = true
15
+ s.homepage = %q{http://code.quirkey.com/restful_query}
16
+ s.rdoc_options = ["--main", "README.rdoc"]
17
+ s.require_paths = ["lib"]
18
+ s.rubyforge_project = %q{quirkey}
19
+ s.rubygems_version = %q{1.3.1}
20
+ s.summary = %q{Simple ActiveRecord queries from a RESTful and safe interface}
21
+ s.test_files = ["test/test_helper.rb", "test/test_restful_query_can_query.rb", "test/test_restful_query_condition.rb", "test/test_restful_query_parser.rb", "test/test_restful_query_sort.rb"]
22
+
23
+ if s.respond_to? :specification_version then
24
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
25
+ s.specification_version = 2
26
+
27
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
28
+ s.add_runtime_dependency(%q<activesupport>, [">= 2.2.0"])
29
+ s.add_runtime_dependency(%q<activerecord>, [">= 2.2.0"])
30
+ s.add_runtime_dependency(%q<chronic>, [">= 0.2.3"])
31
+ s.add_development_dependency(%q<newgem>, [">= 1.2.3"])
32
+ s.add_development_dependency(%q<Shoulda>, [">= 1.2.0"])
33
+ s.add_development_dependency(%q<hoe>, [">= 1.8.0"])
34
+ else
35
+ s.add_dependency(%q<activesupport>, [">= 2.2.0"])
36
+ s.add_dependency(%q<activerecord>, [">= 2.2.0"])
37
+ s.add_dependency(%q<chronic>, [">= 0.2.3"])
38
+ s.add_dependency(%q<newgem>, [">= 1.2.3"])
39
+ s.add_dependency(%q<Shoulda>, [">= 1.2.0"])
40
+ s.add_dependency(%q<hoe>, [">= 1.8.0"])
41
+ end
42
+ else
43
+ s.add_dependency(%q<activesupport>, [">= 2.2.0"])
44
+ s.add_dependency(%q<activerecord>, [">= 2.2.0"])
45
+ s.add_dependency(%q<chronic>, [">= 0.2.3"])
46
+ s.add_dependency(%q<newgem>, [">= 1.2.3"])
47
+ s.add_dependency(%q<Shoulda>, [">= 1.2.0"])
48
+ s.add_dependency(%q<hoe>, [">= 1.8.0"])
49
+ end
50
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :restful_query do
3
+ # # Task goes here
4
+ # end
@@ -0,0 +1,34 @@
1
+ require 'test/unit'
2
+ require 'rubygems'
3
+ require 'shoulda'
4
+
5
+ require File.join(File.dirname(__FILE__), '..', 'lib','restful_query.rb')
6
+
7
+
8
+ unless defined?(ActiveRecord)
9
+ module ActiveRecord
10
+ class Base
11
+ class << self
12
+ attr_accessor :pluralize_table_names
13
+
14
+ def protected_attributes
15
+ []
16
+ end
17
+
18
+ def named_scope(name, options = {})
19
+ end
20
+ end
21
+ self.pluralize_table_names = true
22
+
23
+ include RestfulQuery::CanQuery
24
+ end
25
+ end
26
+ end
27
+
28
+ class ClassWithQuery < ActiveRecord::Base
29
+ can_query
30
+ end
31
+
32
+ class ClassWithoutQuery < ActiveRecord::Base
33
+
34
+ end
@@ -0,0 +1,20 @@
1
+ require File.dirname(__FILE__) + '/test_helper.rb'
2
+
3
+ class TestRestfulQueryCanQuery < Test::Unit::TestCase
4
+
5
+ context "CanQuery" do
6
+ context "A class with the can_query macro" do
7
+ should "can_query?" do
8
+ assert ClassWithQuery.can_query?
9
+ end
10
+ end
11
+
12
+ context "A class without the can_query macro" do
13
+ should "not can_query?" do
14
+ assert !ClassWithoutQuery.can_query?
15
+ end
16
+ end
17
+
18
+ end
19
+
20
+ end
@@ -0,0 +1,112 @@
1
+ require 'test_helper'
2
+
3
+ class RestfulQueryConditionTest < Test::Unit::TestCase
4
+
5
+ context "Condition" do
6
+
7
+ context "initializing" do
8
+ context "with column, value, operator" do
9
+ setup do
10
+ @condition = RestfulQuery::Condition.new('created_at', '1 week ago', '>')
11
+ end
12
+
13
+ should "save column" do
14
+ assert_equal 'created_at', @condition.column
15
+ end
16
+
17
+ should "save value" do
18
+ assert_equal '1 week ago', @condition.value
19
+ end
20
+
21
+ should "save operator as string" do
22
+ assert_equal '>', @condition.operator
23
+ end
24
+
25
+ end
26
+
27
+ context "with no operator" do
28
+ setup do
29
+ @condition = RestfulQuery::Condition.new('created_at', '1 week ago')
30
+ end
31
+
32
+ should "assume =" do
33
+ assert_equal '=', @condition.operator
34
+ end
35
+
36
+ end
37
+
38
+ context "with an operator as a string" do
39
+ setup do
40
+ @condition = RestfulQuery::Condition.new('created_at', '1 week ago', 'gteq')
41
+ end
42
+
43
+ should "translate string to operator" do
44
+ assert_equal '>=', @condition.operator
45
+ end
46
+
47
+ end
48
+
49
+ context "with chronic => true" do
50
+ setup do
51
+ @condition = RestfulQuery::Condition.new('created_at', '1 week ago', 'gteq', :chronic => true)
52
+ end
53
+
54
+ should "save option to options" do
55
+ assert @condition.options[:chronic]
56
+ end
57
+
58
+ should "parse value with chronic" do
59
+ assert_equal(1.week.ago.to_s, @condition.value.to_s)
60
+ end
61
+ end
62
+ end
63
+
64
+
65
+ context "a Condition" do
66
+ setup do
67
+ @condition = RestfulQuery::Condition.new('title', 'Bossman', 'lt')
68
+ end
69
+
70
+ context "with operator = like to condition array" do
71
+ setup do
72
+ @condition = RestfulQuery::Condition.new('title', 'Bossman', 'like')
73
+ @to_condition_array = @condition.to_condition_array
74
+ end
75
+
76
+ should "wrap value with %" do
77
+ assert_equal "%Bossman%", @to_condition_array[1]
78
+ end
79
+
80
+ should "translate operator to LIKE" do
81
+ assert_equal("title LIKE ?", @to_condition_array[0])
82
+ end
83
+
84
+ end
85
+
86
+ context "to_condition_array" do
87
+ setup do
88
+ @to_condition = @condition.to_condition_array
89
+ end
90
+
91
+ should "return array" do
92
+ assert @to_condition.is_a?(Array)
93
+ end
94
+
95
+ should "have conditional string first" do
96
+ assert_equal 'title < ?', @to_condition[0]
97
+ end
98
+
99
+ should "have value as [1]" do
100
+ assert_equal @condition.value, @to_condition[1]
101
+ end
102
+ end
103
+
104
+ context "to_hash" do
105
+ should "return hash like params" do
106
+ assert_equal({'title' => {'lt' => 'Bossman'}}, @condition.to_hash)
107
+ end
108
+ end
109
+ end
110
+ end
111
+
112
+ end
@@ -0,0 +1,392 @@
1
+ require 'test_helper'
2
+
3
+ class RestfulQueryParserTest < Test::Unit::TestCase
4
+
5
+ context "Parser" do
6
+ setup do
7
+ @base_query_hash = {'created_at' => {'gt' => '1 week ago', 'lt' => '1 hour ago'}, 'updated_at' => {'lt' => '1 day ago'}, 'title' => {'eq' => 'Test'}, 'other_time' => {'gt' => 'oct 1'}, 'name' => 'Aaron'}
8
+ end
9
+
10
+ context "from_hash" do
11
+
12
+ context "without hash" do
13
+ setup do
14
+ @parser = RestfulQuery::Parser.new(nil)
15
+ end
16
+
17
+ should "return Parser object" do
18
+ assert @parser.is_a?(RestfulQuery::Parser)
19
+ end
20
+
21
+ should "have a blank hash for query hash" do
22
+ assert_equal({}, @parser.query_hash)
23
+ end
24
+ end
25
+
26
+ context "with a hash of columns and operations" do
27
+ setup do
28
+ new_parser_from_hash
29
+ end
30
+
31
+ should "return parser object" do
32
+ assert @parser.is_a?(RestfulQuery::Parser)
33
+ end
34
+
35
+ should "save hash to query_hash" do
36
+ assert_equal @base_query_hash, @parser.query_hash.to_hash
37
+ end
38
+
39
+ should "save each condition as a condition object" do
40
+ assert @parser.conditions.is_a?(Array)
41
+ assert @parser.conditions.first.is_a?(RestfulQuery::Condition)
42
+ end
43
+
44
+ should "save condition without operator with default operator" do
45
+ assert @parser.conditions_for(:name)
46
+ assert @parser.conditions_for(:name).first.is_a?(RestfulQuery::Condition)
47
+ assert_equal '=', @parser.conditions_for(:name).first.operator
48
+ end
49
+
50
+ end
51
+
52
+ context "with exclude columns" do
53
+ setup do
54
+ new_parser_from_hash({}, :exclude_columns => [:other_time,'name'])
55
+ end
56
+
57
+ should "return parser object" do
58
+ assert @parser.is_a?(RestfulQuery::Parser)
59
+ end
60
+
61
+ should "exclude columns from conditions" do
62
+ assert @parser.conditions_for('created_at')
63
+ assert_nil @parser.conditions_for('other_time')
64
+ assert_nil @parser.conditions_for(:name)
65
+ end
66
+
67
+ end
68
+
69
+ context "with chronic => true" do
70
+ setup do
71
+ new_parser_from_hash({}, :chronic => true)
72
+ end
73
+
74
+ should "return parser object" do
75
+ assert @parser.is_a?(RestfulQuery::Parser)
76
+ end
77
+
78
+ should "parse created at and updated with chronic" do
79
+ assert_equal Chronic.parse('1 week ago').to_s, @parser.conditions_for(:created_at).first.value.to_s
80
+ assert_equal Chronic.parse('1 day ago').to_s, @parser.conditions_for(:updated_at).first.value.to_s
81
+ end
82
+
83
+ end
84
+
85
+ context "with chronic => []" do
86
+ setup do
87
+ new_parser_from_hash({}, :chronic => [:other_time])
88
+ end
89
+
90
+ should "return parser object" do
91
+ assert @parser.is_a?(RestfulQuery::Parser)
92
+ end
93
+
94
+ should "parse selected attributes in array with chronic" do
95
+ assert_equal Chronic.parse('oct 1').to_s, @parser.conditions_for(:other_time).first.value.to_s
96
+ end
97
+
98
+ should "not parse created at/updated at if not specified" do
99
+ assert_not_equal Chronic.parse('1 week ago').to_s, @parser.conditions_for(:created_at).first.value.to_s
100
+ assert_not_equal Chronic.parse('1 day ago').to_s, @parser.conditions_for(:updated_at).first.value.to_s
101
+ end
102
+ end
103
+
104
+ context "with sort as a single string" do
105
+ setup do
106
+ new_parser_from_hash({'_sort' => 'created_at-up'})
107
+ end
108
+
109
+ should "return parser object" do
110
+ assert @parser.is_a?(RestfulQuery::Parser)
111
+ end
112
+
113
+ should "parse sort string" do
114
+ @sort = @parser.sorts.first
115
+ assert @sort.is_a?(RestfulQuery::Sort)
116
+ assert_equal 'ASC', @sort.direction
117
+ assert_equal 'created_at', @sort.column
118
+ end
119
+
120
+ should "add sort to sorts" do
121
+ assert @parser.sorts
122
+ assert_equal 1, @parser.sorts.length
123
+ end
124
+
125
+ end
126
+
127
+ context "with sort as an array of strings" do
128
+ setup do
129
+ new_parser_from_hash({'_sort' => ['created_at-up','title-desc']})
130
+ end
131
+
132
+ should "return parser object" do
133
+ assert @parser.is_a?(RestfulQuery::Parser)
134
+ end
135
+
136
+ should "add sorts to sorts" do
137
+ assert @parser.sorts
138
+ assert_equal 2, @parser.sorts.length
139
+ @parser.sorts.each do |sort|
140
+ assert sort.is_a?(RestfulQuery::Sort)
141
+ end
142
+ end
143
+
144
+ end
145
+
146
+ context "with a default_sort" do
147
+ context "with no sorts defined in the query hash" do
148
+ setup do
149
+ new_parser_from_hash({}, {:default_sort => 'created_at DESC'})
150
+ end
151
+
152
+ should "return parser object" do
153
+ assert @parser.is_a?(RestfulQuery::Parser)
154
+ end
155
+
156
+ should "have default sort in sorts" do
157
+ assert @parser.sorts
158
+ assert_equal 1, @parser.sorts.length
159
+ assert_equal 'created_at DESC', @parser.sort_sql
160
+ end
161
+ end
162
+
163
+ context "with sorts defined in the query hash" do
164
+ setup do
165
+ new_parser_from_hash({'_sort' => 'created_at-up'})
166
+ end
167
+
168
+ should "return parser object" do
169
+ assert @parser.is_a?(RestfulQuery::Parser)
170
+ end
171
+
172
+ should "have query hash sorts in sorts and not default sort" do
173
+ assert @parser.sorts
174
+ assert_equal 1, @parser.sorts.length
175
+ assert_equal 'created_at ASC', @parser.sort_sql
176
+ end
177
+ end
178
+ end
179
+ end
180
+
181
+ context "a loaded parser" do
182
+ setup do
183
+ new_parser_from_hash
184
+ end
185
+
186
+ context "conditions" do
187
+ setup do
188
+ @conditions = @parser.conditions
189
+ end
190
+
191
+ should "return array of all conditions objects" do
192
+ assert @conditions.is_a?(Array)
193
+ @conditions.each do |condition|
194
+ assert condition.is_a?(RestfulQuery::Condition)
195
+ end
196
+ end
197
+
198
+ should "include conditions for every attribute" do
199
+ assert_equal @base_query_hash.keys.length + 1, @conditions.length
200
+ end
201
+ end
202
+
203
+ context "conditions_for" do
204
+ should "return nil for columns without conditions" do
205
+ assert_nil @parser.conditions_for(:blah)
206
+ end
207
+
208
+ should "return array of conditions for column that exists" do
209
+ @conditions = @parser.conditions_for(:created_at)
210
+ assert @conditions.is_a?(Array)
211
+ @conditions.each do |condition|
212
+ assert condition.is_a?(RestfulQuery::Condition)
213
+ assert_equal 'created_at', condition.column
214
+ end
215
+ end
216
+
217
+ end
218
+
219
+ context "to conditions array" do
220
+ setup do
221
+ @conditions = @parser.to_conditions_array
222
+ end
223
+
224
+ should "return array" do
225
+ assert @conditions.is_a?(Array)
226
+ end
227
+
228
+ should "first element should be a condition string" do
229
+ assert @conditions[0].is_a?(String)
230
+ end
231
+
232
+ should "include operators for all querys" do
233
+ assert_match(/(([a-z_]) (\<|\>|\=|\<\=|\>\=) \? AND)+/,@conditions[0])
234
+ end
235
+
236
+ should "join query hash with AND" do
237
+ assert_match(/AND/,@conditions[0])
238
+ end
239
+
240
+ should "include values for each conditions" do
241
+ assert_equal @base_query_hash.keys.length + 2, @conditions.length
242
+ end
243
+
244
+ end
245
+
246
+ context "to conditions with :or" do
247
+ setup do
248
+ @conditions = @parser.to_conditions_array(:or)
249
+ end
250
+
251
+ should "join query hash with OR" do
252
+ assert_match(/(([a-z_]) (\<|\>|\=|\<\=|\>\=) \? OR)+/,@conditions[0])
253
+ end
254
+ end
255
+
256
+ context "to_query_hash" do
257
+ context "with no altering" do
258
+ setup do
259
+ @query_hash = @parser.to_query_hash
260
+ end
261
+
262
+ should "return hash" do
263
+ assert @query_hash.is_a?(Hash)
264
+ end
265
+
266
+ should "return initial query hash" do
267
+ assert_equal({'gt' => '1 week ago', 'lt' => '1 hour ago'}, @query_hash['created_at'])
268
+ end
269
+ end
270
+
271
+ context "with altered sorts" do
272
+ setup do
273
+ @parser.set_sort('title', 'up')
274
+ @parser.set_sort('created_at', 'down')
275
+ @query_hash = @parser.to_query_hash
276
+ end
277
+
278
+ should "include unaltered sort conditions" do
279
+ assert_equal({'gt' => '1 week ago', 'lt' => '1 hour ago'}, @query_hash['created_at'])
280
+ end
281
+
282
+ should "include altered sorts" do
283
+ assert_equal(['title-asc','created_at-desc'], @query_hash['_sort'])
284
+ end
285
+ end
286
+ end
287
+
288
+ context "sorts" do
289
+ setup do
290
+ new_parser_from_hash({'_sort' => ['title-down', 'updated_at-asc']})
291
+ @sorts = @parser.sorts
292
+ end
293
+
294
+ should "return an array of sort objects" do
295
+ assert @sorts
296
+ assert_equal 2, @sorts.length
297
+ @sorts.each do |sort|
298
+ assert sort.is_a?(RestfulQuery::Sort)
299
+ end
300
+ end
301
+
302
+ context "sorted_columns" do
303
+ should "return an array of columns" do
304
+ @sorted_columns = @parser.sorted_columns
305
+ assert @sorted_columns.is_a?(Array)
306
+ assert @sorted_columns.include?('title')
307
+ end
308
+ end
309
+
310
+ context "sorted_by?" do
311
+ should "return true if column is sorted" do
312
+ assert @parser.sorted_by?('title')
313
+ end
314
+
315
+ should "return false if column is not sorted" do
316
+ assert !@parser.sorted_by?('created_at')
317
+ end
318
+ end
319
+
320
+ context "sort()" do
321
+ should "return Sort object if column is sorted" do
322
+ sort = @parser.sort('title')
323
+ assert sort.is_a?(RestfulQuery::Sort)
324
+ assert_equal 'title', sort.column
325
+ end
326
+
327
+ should "return nil if col" do
328
+ assert_nil @parser.sort('created_at')
329
+ end
330
+ end
331
+
332
+ context "set_sort" do
333
+ context "with an existing sort" do
334
+ setup do
335
+ @parser.set_sort('title','up')
336
+ end
337
+
338
+ should "not add new sort" do
339
+ assert_equal 2, @parser.sorts.length
340
+ end
341
+
342
+ should "update sort direction" do
343
+ assert_equal 'ASC', @parser.sort('title').direction
344
+ end
345
+ end
346
+
347
+ context "with direction: nil" do
348
+ setup do
349
+ @parser.set_sort('title', nil)
350
+ end
351
+
352
+ should "remove sort" do
353
+ assert_equal 1, @parser.sorts.length
354
+ assert !@parser.sorted_by?('title')
355
+ end
356
+ end
357
+
358
+ context "with a new sort" do
359
+ setup do
360
+ @parser.set_sort('name', 'down')
361
+ end
362
+
363
+ should "add sort to sorts" do
364
+ assert_equal 3, @parser.sorts.length
365
+ end
366
+
367
+ should "set sort direction" do
368
+ assert_equal 'DESC', @parser.sort('name').direction
369
+ end
370
+ end
371
+
372
+ end
373
+
374
+ end
375
+
376
+
377
+ context "sort_sql" do
378
+ should "join order with ," do
379
+ new_parser_from_hash({'_sort' => ['title-down', 'updated_at-asc']})
380
+ assert_equal 'title DESC, updated_at ASC', @parser.sort_sql
381
+ end
382
+ end
383
+
384
+ end
385
+
386
+ end
387
+
388
+ protected
389
+ def new_parser_from_hash(params = {}, options = {})
390
+ @parser = RestfulQuery::Parser.new(@base_query_hash.merge(params), options)
391
+ end
392
+ end
@@ -0,0 +1,72 @@
1
+ require 'test_helper'
2
+
3
+ class RestfulQuerySortTest < Test::Unit::TestCase
4
+
5
+ context "Sort" do
6
+ context "initializing" do
7
+ context "with valid column and direction" do
8
+ setup do
9
+ @sort = RestfulQuery::Sort.new(:attribute, 'up')
10
+ end
11
+
12
+ should "save column name as string" do
13
+ assert_equal 'attribute', @sort.column
14
+ end
15
+
16
+ should "interpret direction" do
17
+ assert_equal 'ASC', @sort.direction
18
+ end
19
+ end
20
+
21
+ context "with an invalid direction" do
22
+ should "raise error" do
23
+ assert_raise(RestfulQuery::InvalidDirection) do
24
+ RestfulQuery::Sort.new('column', 'blarg')
25
+ end
26
+ end
27
+ end
28
+
29
+ end
30
+
31
+ context "parse" do
32
+ context "with a query hash like condition" do
33
+ setup do
34
+ @sort = RestfulQuery::Sort.parse('long_name_attribute-down')
35
+ end
36
+
37
+ should "return sort object" do
38
+ assert @sort.is_a?(RestfulQuery::Sort)
39
+ end
40
+
41
+ should "set column and direction" do
42
+ assert_equal 'long_name_attribute', @sort.column
43
+ assert_equal 'DESC', @sort.direction
44
+ end
45
+ end
46
+
47
+ context "with a standard SQL like condition" do
48
+ setup do
49
+ @sort = RestfulQuery::Sort.parse('long_name_attribute DESC')
50
+ end
51
+
52
+ should "return sort object" do
53
+ assert @sort.is_a?(RestfulQuery::Sort)
54
+ end
55
+
56
+ should "set column and direction" do
57
+ assert_equal 'long_name_attribute', @sort.column
58
+ assert_equal 'DESC', @sort.direction
59
+ end
60
+ end
61
+ end
62
+
63
+ context "to_sql" do
64
+ should "join the column and attribute" do
65
+ @sort = RestfulQuery::Sort.new(:attribute, 'down')
66
+ assert_equal 'attribute DESC', @sort.to_sql
67
+ end
68
+ end
69
+
70
+ end
71
+
72
+ end
metadata ADDED
@@ -0,0 +1,138 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: restful_query
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - Aaron Quint
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-03-21 00:00:00 -04:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: activesupport
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 2.2.0
24
+ version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: activerecord
27
+ type: :runtime
28
+ version_requirement:
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 2.2.0
34
+ version:
35
+ - !ruby/object:Gem::Dependency
36
+ name: chronic
37
+ type: :runtime
38
+ version_requirement:
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: 0.2.3
44
+ version:
45
+ - !ruby/object:Gem::Dependency
46
+ name: newgem
47
+ type: :development
48
+ version_requirement:
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: 1.2.3
54
+ version:
55
+ - !ruby/object:Gem::Dependency
56
+ name: Shoulda
57
+ type: :development
58
+ version_requirement:
59
+ version_requirements: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: 1.2.0
64
+ version:
65
+ - !ruby/object:Gem::Dependency
66
+ name: hoe
67
+ type: :development
68
+ version_requirement:
69
+ version_requirements: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: 1.8.0
74
+ version:
75
+ description: Simple ActiveRecord queries from a RESTful and safe interface
76
+ email:
77
+ - aaron@quirkey.com
78
+ executables: []
79
+
80
+ extensions: []
81
+
82
+ extra_rdoc_files:
83
+ - History.txt
84
+ - Manifest.txt
85
+ - README.rdoc
86
+ files:
87
+ - History.txt
88
+ - LICENSE
89
+ - Manifest.txt
90
+ - README.rdoc
91
+ - Rakefile
92
+ - init.rb
93
+ - lib/restful_query.rb
94
+ - lib/restful_query/can_query.rb
95
+ - lib/restful_query/condition.rb
96
+ - lib/restful_query/parser.rb
97
+ - lib/restful_query/sort.rb
98
+ - rails/init.rb
99
+ - restful_query.gemspec
100
+ - tasks/restful_query_tasks.rake
101
+ - test/test_helper.rb
102
+ - test/test_restful_query_can_query.rb
103
+ - test/test_restful_query_condition.rb
104
+ - test/test_restful_query_parser.rb
105
+ - test/test_restful_query_sort.rb
106
+ has_rdoc: true
107
+ homepage: http://code.quirkey.com/restful_query
108
+ post_install_message:
109
+ rdoc_options:
110
+ - --main
111
+ - README.rdoc
112
+ require_paths:
113
+ - lib
114
+ required_ruby_version: !ruby/object:Gem::Requirement
115
+ requirements:
116
+ - - ">="
117
+ - !ruby/object:Gem::Version
118
+ version: "0"
119
+ version:
120
+ required_rubygems_version: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: "0"
125
+ version:
126
+ requirements: []
127
+
128
+ rubyforge_project: quirkey
129
+ rubygems_version: 1.3.1
130
+ signing_key:
131
+ specification_version: 2
132
+ summary: Simple ActiveRecord queries from a RESTful and safe interface
133
+ test_files:
134
+ - test/test_helper.rb
135
+ - test/test_restful_query_can_query.rb
136
+ - test/test_restful_query_condition.rb
137
+ - test/test_restful_query_parser.rb
138
+ - test/test_restful_query_sort.rb