restful_query 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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