twilson63-sinatra-squirrel 0.1.2
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/.document +5 -0
- data/.gitignore +5 -0
- data/LICENSE +20 -0
- data/README.rdoc +9 -0
- data/Rakefile +56 -0
- data/VERSION.yml +4 -0
- data/lib/sinatra/extensions.rb +36 -0
- data/lib/sinatra/paginator.rb +81 -0
- data/lib/sinatra/squirrel.rb +537 -0
- data/sinatra-squirrel.gemspec +48 -0
- data/test/sinatra-squirrel_test.rb +7 -0
- data/test/test_helper.rb +10 -0
- metadata +66 -0
data/.document
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 twilson63
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
data/Rakefile
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "sinatra-squirrel"
|
8
|
+
gem.summary = %Q{Port of Thought Bot Squirrel to Sinatra}
|
9
|
+
gem.email = "tom@jackrussellsoftware.com"
|
10
|
+
gem.homepage = "http://github.com/twilson63/sinatra-squirrel"
|
11
|
+
gem.authors = ["twilson63"]
|
12
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
13
|
+
end
|
14
|
+
|
15
|
+
rescue LoadError
|
16
|
+
puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
|
17
|
+
end
|
18
|
+
|
19
|
+
require 'rake/testtask'
|
20
|
+
Rake::TestTask.new(:test) do |test|
|
21
|
+
test.libs << 'lib' << 'test'
|
22
|
+
test.pattern = 'test/**/*_test.rb'
|
23
|
+
test.verbose = true
|
24
|
+
end
|
25
|
+
|
26
|
+
begin
|
27
|
+
require 'rcov/rcovtask'
|
28
|
+
Rcov::RcovTask.new do |test|
|
29
|
+
test.libs << 'test'
|
30
|
+
test.pattern = 'test/**/*_test.rb'
|
31
|
+
test.verbose = true
|
32
|
+
end
|
33
|
+
rescue LoadError
|
34
|
+
task :rcov do
|
35
|
+
abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
task :default => :test
|
41
|
+
|
42
|
+
require 'rake/rdoctask'
|
43
|
+
Rake::RDocTask.new do |rdoc|
|
44
|
+
if File.exist?('VERSION.yml')
|
45
|
+
config = YAML.load(File.read('VERSION.yml'))
|
46
|
+
version = "#{config[:major]}.#{config[:minor]}.#{config[:patch]}"
|
47
|
+
else
|
48
|
+
version = ""
|
49
|
+
end
|
50
|
+
|
51
|
+
rdoc.rdoc_dir = 'rdoc'
|
52
|
+
rdoc.title = "sinatra-squirrel #{version}"
|
53
|
+
rdoc.rdoc_files.include('README*')
|
54
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
55
|
+
end
|
56
|
+
|
data/VERSION.yml
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
class Hash
|
2
|
+
def merge_tree other
|
3
|
+
self.dup.merge_tree! other
|
4
|
+
end
|
5
|
+
|
6
|
+
def merge_tree! other
|
7
|
+
other.each do |key, value|
|
8
|
+
if self[key].is_a?(Hash) && value.is_a?(Hash)
|
9
|
+
self[key] = self[key].merge_tree(value)
|
10
|
+
else
|
11
|
+
self[key] = value
|
12
|
+
end
|
13
|
+
end
|
14
|
+
self
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
module ActiveRecord #:nodoc: all
|
19
|
+
module Associations
|
20
|
+
module ClassMethods
|
21
|
+
class JoinDependency
|
22
|
+
class JoinAssociation
|
23
|
+
def ancestry #:doc
|
24
|
+
[ parent.ancestry, reflection.name ].flatten.compact
|
25
|
+
end
|
26
|
+
end
|
27
|
+
class JoinBase
|
28
|
+
def ancestry
|
29
|
+
nil
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
@@ -0,0 +1,81 @@
|
|
1
|
+
module Squirrel
|
2
|
+
# The WillPagination module emulates the results that the will_paginate plugin returns
|
3
|
+
# from its #paginate methods. When it is used to extend a result set from Squirrel, it
|
4
|
+
# automtically pulls the result from the pagination that Squirrel performs. The methods
|
5
|
+
# added to that result set make it duck-equivalent to the WillPaginate::Collection
|
6
|
+
# class.
|
7
|
+
module WillPagination
|
8
|
+
def self.extended base
|
9
|
+
base.current_page = base.pages.current || 1
|
10
|
+
base.per_page = base.pages.per_page
|
11
|
+
base.total_entries = base.pages.total_results
|
12
|
+
end
|
13
|
+
|
14
|
+
attr_accessor :current_page, :per_page, :total_entries
|
15
|
+
|
16
|
+
# Returns the current_page + 1, or nil if there are no more.
|
17
|
+
def next_page
|
18
|
+
current_page < page_count ? (current_page + 1) : nil
|
19
|
+
end
|
20
|
+
|
21
|
+
# Returns the offset of the current page that is suitable for inserting into SQL.
|
22
|
+
def offset
|
23
|
+
(current_page - 1) * per_page
|
24
|
+
end
|
25
|
+
|
26
|
+
# Returns true if the current_page is greater than the total number of pages.
|
27
|
+
# Useful in case someone manually modifies the URL to put their own page number in.
|
28
|
+
def out_of_bounds?
|
29
|
+
current_page > page_count
|
30
|
+
end
|
31
|
+
|
32
|
+
# The number of pages in the result set.
|
33
|
+
def page_count
|
34
|
+
pages.last
|
35
|
+
end
|
36
|
+
|
37
|
+
alias_method :total_pages, :page_count
|
38
|
+
|
39
|
+
# Returns the current_page - 1, or nil if this is the first page.
|
40
|
+
def previous_page
|
41
|
+
current_page > 1 ? (current_page - 1) : nil
|
42
|
+
end
|
43
|
+
|
44
|
+
# Sets the number of pages and entries.
|
45
|
+
def total_entries= total
|
46
|
+
@total_entries = total.to_i
|
47
|
+
@total_pages = (@total_entries / per_page.to_f).ceil
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# The Page class holds information about the current page of results.
|
52
|
+
class Page
|
53
|
+
attr_reader :offset, :limit, :page, :per_page
|
54
|
+
def initialize(offset, limit, page, per_page)
|
55
|
+
@offset, @limit, @page, @per_page = offset, limit, page, per_page
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# A Paginator object is what gets inserted into the result set and is returned by
|
60
|
+
# the #pages method of the result set. Contains offets and limits for all pages.
|
61
|
+
class Paginator < Array
|
62
|
+
attr_reader :total_results, :per_page, :current, :next, :previous, :first, :last, :current_range
|
63
|
+
def initialize opts={}
|
64
|
+
@total_results = opts[:count].to_i
|
65
|
+
@limit = opts[:limit].to_i
|
66
|
+
@offset = opts[:offset].to_i
|
67
|
+
|
68
|
+
@per_page = @limit
|
69
|
+
@current = (@offset / @limit) + 1
|
70
|
+
@first = 1
|
71
|
+
@last = ((@total_results-1) / @limit) + 1
|
72
|
+
@next = @current + 1 if @current < @last
|
73
|
+
@previous = @current - 1 if @current > 1
|
74
|
+
@current_range = ((@offset+1)..([@offset+@limit, @total_results].min))
|
75
|
+
|
76
|
+
(@first..@last).each do |page|
|
77
|
+
self[page-1] = Page.new((page-1) * @per_page, @per_page, page, @per_page)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,537 @@
|
|
1
|
+
|
2
|
+
require File.dirname(__FILE__) + '/paginator.rb'
|
3
|
+
|
4
|
+
# Squirrel is a library for making querying the database using ActiveRecord cleaner, easier
|
5
|
+
# to read, and less prone to user error. It does this by allowing AR::Base#find to take a block,
|
6
|
+
# which is run to build the conditions and includes required to execute the query.
|
7
|
+
module Squirrel
|
8
|
+
# When included in AR::Base, it chains the #find method to allow for block execution.
|
9
|
+
module Hook
|
10
|
+
def find_with_squirrel *args, &blk
|
11
|
+
args ||= [:all]
|
12
|
+
if blk || (args.last.is_a?(Hash) && args.last.has_key?(:paginate))
|
13
|
+
query = Query.new(self, &blk)
|
14
|
+
query.execute(*args)
|
15
|
+
else
|
16
|
+
find_without_squirrel(*args)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def scoped_with_squirrel *args, &blk
|
21
|
+
if blk
|
22
|
+
query = Query.new(self, &blk)
|
23
|
+
self.scoped(query.to_find_parameters)
|
24
|
+
else
|
25
|
+
scoped_without_squirrel(*args)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.included base
|
30
|
+
if ! base.instance_methods.include?('find_without_squirrel') &&
|
31
|
+
base.instance_methods.include?('find')
|
32
|
+
base.class_eval do
|
33
|
+
alias_method :find_without_squirrel, :find
|
34
|
+
alias_method :find, :find_with_squirrel
|
35
|
+
end
|
36
|
+
end
|
37
|
+
if ! base.instance_methods.include?('scoped_without_squirrel') &&
|
38
|
+
base.instance_methods.include?('scoped')
|
39
|
+
base.class_eval do
|
40
|
+
alias_method :scoped_without_squirrel, :scoped
|
41
|
+
alias_method :scoped, :scoped_with_squirrel
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
module NamedScopeHook
|
48
|
+
def scoped *args, &blk
|
49
|
+
args = blk ? [Query.new(self, &blk).to_find_parameters] : args
|
50
|
+
scopes[:scoped].call(self, *args)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# The Query is what contains the query and is what handles execution and pagination of the
|
55
|
+
# result set.
|
56
|
+
class Query
|
57
|
+
attr_reader :conditions, :model
|
58
|
+
|
59
|
+
# Creates a Query specific to the given model (which is a class that descends from AR::Base)
|
60
|
+
# and a block that will be run to find the conditions for the #find call.
|
61
|
+
def initialize model, &blk
|
62
|
+
@model = model
|
63
|
+
@joins = nil
|
64
|
+
@binding = blk && blk.binding
|
65
|
+
@conditions = ConditionGroup.new(@model, "AND", @binding, &blk)
|
66
|
+
@conditions.assign_joins( join_dependency )
|
67
|
+
end
|
68
|
+
|
69
|
+
# Builds the dependencies needed to find what AR plans to call the tables in the query
|
70
|
+
# by finding and sending what would be passed in as the +include+ parameter to #find.
|
71
|
+
# This is a necessary step because what AR calls tables deeply nested might not be
|
72
|
+
# completely obvious.)
|
73
|
+
def join_dependency
|
74
|
+
jd = ::ActiveRecord::Associations::ClassMethods::JoinDependency
|
75
|
+
@join_dependency ||= jd.new model,
|
76
|
+
@conditions.to_find_include,
|
77
|
+
nil
|
78
|
+
end
|
79
|
+
|
80
|
+
# Runs the block which builds the conditions. If requested, paginates the result_set.
|
81
|
+
# If the first parameter to #find is :query (instead of :all or :first), then the query
|
82
|
+
# object itself is returned. Useful for debugging, but not much else.
|
83
|
+
def execute *args
|
84
|
+
if args.first == :query
|
85
|
+
self
|
86
|
+
else
|
87
|
+
opts = args.last.is_a?(Hash) ? args.last : {}
|
88
|
+
results = []
|
89
|
+
pagination = opts.delete(:paginate) || {}
|
90
|
+
model.send(:with_scope, :find => opts) do
|
91
|
+
@conditions.paginate(pagination) unless pagination.empty?
|
92
|
+
results = model.find args[0], to_find_parameters
|
93
|
+
if @conditions.paginate?
|
94
|
+
paginate_result_set results, to_find_parameters
|
95
|
+
end
|
96
|
+
end
|
97
|
+
results
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def to_find_parameters
|
102
|
+
find_parameters = {}
|
103
|
+
find_parameters[:conditions] = to_find_conditions unless to_find_conditions.blank?
|
104
|
+
find_parameters[:include ] = to_find_include unless to_find_include.blank?
|
105
|
+
find_parameters[:order ] = to_find_order unless to_find_order.blank?
|
106
|
+
find_parameters[:limit ] = to_find_limit unless to_find_limit.blank?
|
107
|
+
find_parameters[:offset ] = to_find_offset unless to_find_offset.blank?
|
108
|
+
find_parameters
|
109
|
+
end
|
110
|
+
|
111
|
+
# Delegates the to_find_conditions call to the root ConditionGroup
|
112
|
+
def to_find_conditions
|
113
|
+
@conditions.to_find_conditions
|
114
|
+
end
|
115
|
+
|
116
|
+
# Delegates the to_find_include call to the root ConditionGroup
|
117
|
+
def to_find_include
|
118
|
+
@conditions.to_find_include
|
119
|
+
end
|
120
|
+
|
121
|
+
# Delegates the to_find_order call to the root ConditionGroup
|
122
|
+
def to_find_order
|
123
|
+
@conditions.to_find_order
|
124
|
+
end
|
125
|
+
|
126
|
+
# Delegates the to_find_limit call to the root ConditionGroup
|
127
|
+
def to_find_limit
|
128
|
+
@conditions.to_find_limit
|
129
|
+
end
|
130
|
+
|
131
|
+
# Delegates the to_find_offset call to the root ConditionGroup
|
132
|
+
def to_find_offset
|
133
|
+
@conditions.to_find_offset
|
134
|
+
end
|
135
|
+
|
136
|
+
# Used by #execute to paginates the result set if
|
137
|
+
# pagination was requested. In this case, it adds +pages+ and +total_results+ accessors
|
138
|
+
# to the result set. See Paginator for more details.
|
139
|
+
def paginate_result_set set, conditions
|
140
|
+
limit = conditions.delete(:limit)
|
141
|
+
offset = conditions.delete(:offset)
|
142
|
+
|
143
|
+
class << set
|
144
|
+
attr_reader :pages
|
145
|
+
attr_reader :total_results
|
146
|
+
end
|
147
|
+
|
148
|
+
total_results = model.count(conditions)
|
149
|
+
set.instance_variable_set("@pages",
|
150
|
+
Paginator.new( :count => total_results,
|
151
|
+
:limit => limit,
|
152
|
+
:offset => offset) )
|
153
|
+
set.instance_variable_set("@total_results", total_results)
|
154
|
+
set.extend( Squirrel::WillPagination )
|
155
|
+
end
|
156
|
+
|
157
|
+
# ConditionGroups are groups of Conditions, oddly enough. They most closely map to models
|
158
|
+
# in your schema, but they also handle the grouping jobs for the #any and #all blocks.
|
159
|
+
class ConditionGroup
|
160
|
+
attr_accessor :model, :logical_join, :binding, :reflection, :path
|
161
|
+
|
162
|
+
# Creates a ConditionGroup by passing in the following arguments:
|
163
|
+
# * model: The AR subclass that defines what columns and associations will be accessible
|
164
|
+
# in the given block.
|
165
|
+
# * logical_join: A string containing the join that will be used when concatenating the
|
166
|
+
# conditions together. The root level ConditionGroup created by Query defaults the
|
167
|
+
# join to be "AND", but the #any and #all methods will create specific ConditionGroups
|
168
|
+
# using "OR" and "AND" as their join, respectively.
|
169
|
+
# * binding: The +binding+ of the block passed to the original #find. Will be used to
|
170
|
+
# +eval+ what +self+ would be. This is necessary for using methods like +params+ and
|
171
|
+
# +session+ in your controllers.
|
172
|
+
# * path: The "path" taken through the models to arrive at this model. For example, if
|
173
|
+
# your User class has_many Posts which has_many Comments each of which belongs_to User,
|
174
|
+
# the path to the second User would be [:posts, :comments, :user]
|
175
|
+
# * reflection: The association used to get to this block. If nil, then no new association
|
176
|
+
# was traversed, which means we're in an #any or #all grouping block.
|
177
|
+
# * blk: The block to be executed.
|
178
|
+
#
|
179
|
+
# This method defines a number of methods to be available inside the block, one for each
|
180
|
+
# of the columns and associations in the specified model. Note that you CANNOT use
|
181
|
+
# user-defined methods on your model inside Squirrel queries. They don't have any meaning
|
182
|
+
# in the context of a database query.
|
183
|
+
def initialize model, logical_join, binding, path = nil, reflection = nil, &blk
|
184
|
+
@model = model
|
185
|
+
@logical_join = logical_join
|
186
|
+
@conditions = []
|
187
|
+
@condition_blocks = []
|
188
|
+
@reflection = reflection
|
189
|
+
@path = [ path, reflection ].compact.flatten
|
190
|
+
@binding = binding
|
191
|
+
@order = []
|
192
|
+
@negative = false
|
193
|
+
@paginator = false
|
194
|
+
@block = blk
|
195
|
+
|
196
|
+
existing_methods = self.class.instance_methods(false)
|
197
|
+
(model.column_names - existing_methods).each do |col|
|
198
|
+
(class << self; self; end).class_eval do
|
199
|
+
define_method(col.to_s.intern) do
|
200
|
+
column(col)
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
204
|
+
(model.reflections.keys - existing_methods).each do |assn|
|
205
|
+
(class << self; self; end).class_eval do
|
206
|
+
define_method(assn.to_s.intern) do
|
207
|
+
association(assn)
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
execute_block
|
213
|
+
end
|
214
|
+
|
215
|
+
# Creates a Condition and queues it for inclusion. When calling a method defined
|
216
|
+
# during the creation of the ConditionGroup object is the same as calling column(:column_name).
|
217
|
+
# This is useful if you need to access a column that happens to coincide with the name of
|
218
|
+
# an already-defined method (e.g. anything returned by instance_methods(false) for the
|
219
|
+
# given model).
|
220
|
+
def column name
|
221
|
+
@conditions << Condition.new(name)
|
222
|
+
@conditions.last
|
223
|
+
end
|
224
|
+
|
225
|
+
# Similar to #column, this will create an association even if you can't use the normal
|
226
|
+
# method version.
|
227
|
+
def association name, &blk
|
228
|
+
name = name.to_s.intern
|
229
|
+
ref = @model.reflect_on_association(name)
|
230
|
+
@condition_blocks << ConditionGroup.new(ref.klass, logical_join, binding, path, ref.name, &blk)
|
231
|
+
@condition_blocks.last
|
232
|
+
end
|
233
|
+
|
234
|
+
# Creates a ConditionGroup that has the logical_join set to "OR".
|
235
|
+
def any &blk
|
236
|
+
@condition_blocks << ConditionGroup.new(model, "OR", binding, path, &blk)
|
237
|
+
@condition_blocks.last
|
238
|
+
end
|
239
|
+
|
240
|
+
# Creates a ConditionGroup that has the logical_join set to "AND".
|
241
|
+
def all &blk
|
242
|
+
@condition_blocks << ConditionGroup.new(model, "AND", binding, path, &blk)
|
243
|
+
@condition_blocks.last
|
244
|
+
end
|
245
|
+
|
246
|
+
# Sets the arguments for the :order parameter. Arguments can be columns (i.e. Conditions)
|
247
|
+
# or they can be strings (for "RANDOM()", etc.). If a Condition is used, and the column is
|
248
|
+
# negated using #not or #desc, then the resulting specification in the ORDER clause will
|
249
|
+
# be ordered descending. That is, "order_by name.desc" will become "ORDER name DESC"
|
250
|
+
def order_by *columns
|
251
|
+
@order += [columns].flatten
|
252
|
+
end
|
253
|
+
|
254
|
+
# Flags the result set to be paginated according to the :page and :per_page parameters
|
255
|
+
# to this method.
|
256
|
+
def paginate opts = {}
|
257
|
+
@paginator = true
|
258
|
+
page = (opts[:page] || 1).to_i
|
259
|
+
per_page = (opts[:per_page] || 20).to_i
|
260
|
+
page = 1 if page < 1
|
261
|
+
limit( per_page, ( page - 1 ) * per_page )
|
262
|
+
end
|
263
|
+
|
264
|
+
# Similar to #paginate, but does not flag the result set for pagination. Takes a limit
|
265
|
+
# and an offset (by default the offset is 0).
|
266
|
+
def limit lim, off = nil
|
267
|
+
@limit = ( lim || @limit ).to_i
|
268
|
+
@offset = ( off || @offset ).to_i
|
269
|
+
end
|
270
|
+
|
271
|
+
# Returns true if this ConditionGroup or any of its subgroups have been flagged for pagination.
|
272
|
+
def paginate?
|
273
|
+
@paginator || @condition_blocks.any?(&:paginate?)
|
274
|
+
end
|
275
|
+
|
276
|
+
# Negates the condition. Essentially prefixes the condition with NOT in the final query.
|
277
|
+
def -@
|
278
|
+
@negative = !@negative
|
279
|
+
self
|
280
|
+
end
|
281
|
+
|
282
|
+
alias_method :desc, :-@
|
283
|
+
|
284
|
+
# Negates the condition. Also works to negate ConditionGroup blocks in a more straightforward
|
285
|
+
# manner, like so:
|
286
|
+
# any.not do
|
287
|
+
# id == 1
|
288
|
+
# name == "Joe"
|
289
|
+
# end
|
290
|
+
#
|
291
|
+
# # => "NOT( id = 1 OR name = 'Joe')"
|
292
|
+
def not &blk
|
293
|
+
@negative = !@negative
|
294
|
+
if blk
|
295
|
+
@block = blk
|
296
|
+
execute_block
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
# Takes the JoinDependency object and filters it down through the ConditionGroups
|
301
|
+
# to make sure each one knows the aliases necessary to refer to each table by its
|
302
|
+
# correct name.
|
303
|
+
def assign_joins join_dependency, ancestries = nil
|
304
|
+
ancestries ||= join_dependency.join_associations.map{|ja| ja.ancestry }
|
305
|
+
unless @conditions.empty?
|
306
|
+
my_association = unless @path.blank?
|
307
|
+
join_dependency.join_associations[ancestries.index(@path)]
|
308
|
+
else
|
309
|
+
join_dependency.join_base
|
310
|
+
end
|
311
|
+
@conditions.each do |column|
|
312
|
+
column.assign_join(my_association)
|
313
|
+
end
|
314
|
+
end
|
315
|
+
@condition_blocks.each do |association|
|
316
|
+
association.assign_joins(join_dependency, ancestries)
|
317
|
+
end
|
318
|
+
end
|
319
|
+
|
320
|
+
# Generates the parameter for :include for this ConditionGroup and all its subgroups.
|
321
|
+
def to_find_include
|
322
|
+
@condition_blocks.inject({}) do |inc, cb|
|
323
|
+
if cb.reflection.nil?
|
324
|
+
inc.merge_tree(cb.to_find_include)
|
325
|
+
else
|
326
|
+
inc[cb.reflection] ||= {}
|
327
|
+
inc[cb.reflection] = inc[cb.reflection].merge_tree(cb.to_find_include)
|
328
|
+
inc
|
329
|
+
end
|
330
|
+
end
|
331
|
+
end
|
332
|
+
|
333
|
+
# Generates the :order parameter for this ConditionGroup. Because this does not reference
|
334
|
+
# subgroups it should only be used from the outermost block (which is probably where it makes
|
335
|
+
# the most sense to reference it, but it's worth mentioning)
|
336
|
+
def to_find_order
|
337
|
+
if @order.blank?
|
338
|
+
nil
|
339
|
+
else
|
340
|
+
@order.collect do |col|
|
341
|
+
col.respond_to?(:full_name) ? (col.full_name + (col.negative? ? " DESC" : "")) : col
|
342
|
+
end.join(", ")
|
343
|
+
end
|
344
|
+
end
|
345
|
+
|
346
|
+
# Generates the :conditions parameter for this ConditionGroup and all subgroups. It
|
347
|
+
# generates them in ["sql", params] format because of the requirements of LIKE, etc.
|
348
|
+
def to_find_conditions
|
349
|
+
segments = conditions.collect{|c| c.to_find_conditions }.compact
|
350
|
+
return nil if segments.length == 0
|
351
|
+
cond = "(" + segments.collect{|s| s.first }.join(" #{logical_join} ") + ")"
|
352
|
+
cond = "NOT #{cond}" if negative?
|
353
|
+
|
354
|
+
values = segments.inject([]){|all, now| all + now[1..-1] }
|
355
|
+
[ cond, *values ]
|
356
|
+
end
|
357
|
+
|
358
|
+
# Generates the :limit parameter.
|
359
|
+
def to_find_limit
|
360
|
+
@limit
|
361
|
+
end
|
362
|
+
|
363
|
+
# Generates the :offset parameter.
|
364
|
+
def to_find_offset
|
365
|
+
@offset
|
366
|
+
end
|
367
|
+
|
368
|
+
# Returns all the conditions, which is the union of the Conditions and ConditionGroups
|
369
|
+
# that belong to this ConditionGroup.
|
370
|
+
def conditions
|
371
|
+
@conditions + @condition_blocks
|
372
|
+
end
|
373
|
+
|
374
|
+
# Returns true if this block has been negated using #not, #desc, or #-
|
375
|
+
def negative?
|
376
|
+
@negative
|
377
|
+
end
|
378
|
+
|
379
|
+
# This is a bit of a hack, due to how Squirrel is built. It can be used to fetch
|
380
|
+
# instance variables from the location where the call to #find was made. For example,
|
381
|
+
# if called from within your model and you happened to have an instance variable called
|
382
|
+
# "@foo", you can access it by calling
|
383
|
+
# instance "@foo"
|
384
|
+
# from within your Squirrel query.
|
385
|
+
def instance instance_var
|
386
|
+
s = eval("self", binding)
|
387
|
+
if s
|
388
|
+
s.instance_variable_get(instance_var)
|
389
|
+
end
|
390
|
+
end
|
391
|
+
|
392
|
+
private
|
393
|
+
|
394
|
+
def execute_block #:nodoc:
|
395
|
+
instance_eval &@block if @block
|
396
|
+
end
|
397
|
+
|
398
|
+
def method_missing meth, *args #:nodoc:
|
399
|
+
m = eval <<-end_eval, binding
|
400
|
+
begin
|
401
|
+
method(:#{meth})
|
402
|
+
rescue NameError
|
403
|
+
nil
|
404
|
+
end
|
405
|
+
end_eval
|
406
|
+
if m
|
407
|
+
m.call(*args)
|
408
|
+
else
|
409
|
+
super(meth, *args)
|
410
|
+
end
|
411
|
+
end
|
412
|
+
|
413
|
+
end
|
414
|
+
|
415
|
+
# Handles comparisons in the query. This class is analagous to the columns in the database.
|
416
|
+
# When comparing the Condition to a value, the operators are used as follows:
|
417
|
+
# * ==, === : Straight-up Equals. Can also be used as the "IN" operator if the operand is an Array.
|
418
|
+
# Additionally, when the oprand is +nil+, the comparison is correctly generates as "IS NULL"."
|
419
|
+
# * =~ : The LIKE and REGEXP operators. If the operand is a String, it will generate a LIKE
|
420
|
+
# comparison. If it is a Regexp, the REGEXP operator will be used. NOTE: MySQL regular expressions
|
421
|
+
# are NOT the same as Ruby regular expressions. Also NOTE: No wildcards are inserted into the LIKE
|
422
|
+
# comparison, so you may add them where you wish.
|
423
|
+
# * <=> : Performs a BETWEEN comparison, as long as the operand responds to both #first and #last,
|
424
|
+
# which both Ranges and Arrays do.
|
425
|
+
# * > : A simple greater-than comparison.
|
426
|
+
# * >= : Greater-than or equal-to.
|
427
|
+
# * < : A simple less-than comparison.
|
428
|
+
# * <= : Less-than or equal-to.
|
429
|
+
# * contains? : Like =~, except automatically surrounds the operand in %s, which =~ does not do.
|
430
|
+
# * nil? : Works exactly like "column == nil", but in a nicer syntax, which is what Squirrel is all about.
|
431
|
+
class Condition
|
432
|
+
attr_reader :name, :operator, :operand
|
433
|
+
|
434
|
+
# Creates and Condition with the given name.
|
435
|
+
def initialize name
|
436
|
+
@name = name
|
437
|
+
@sql = nil
|
438
|
+
@negative = false
|
439
|
+
end
|
440
|
+
|
441
|
+
[ :==, :===, :=~, :<=>, :<=, :<, :>, :>= ].each do |op|
|
442
|
+
define_method(op) do |val|
|
443
|
+
@operator = op
|
444
|
+
@operand = val
|
445
|
+
self
|
446
|
+
end
|
447
|
+
end
|
448
|
+
|
449
|
+
def contains? val #:nodoc:
|
450
|
+
@operator = :contains
|
451
|
+
@operand = val
|
452
|
+
self
|
453
|
+
end
|
454
|
+
|
455
|
+
def nil? #:nodoc:
|
456
|
+
@operator = :==
|
457
|
+
@operand = nil
|
458
|
+
self
|
459
|
+
end
|
460
|
+
|
461
|
+
def -@ #:nodoc:
|
462
|
+
@negative = !@negative
|
463
|
+
self
|
464
|
+
end
|
465
|
+
|
466
|
+
alias_method :not, :-@
|
467
|
+
alias_method :desc, :-@
|
468
|
+
|
469
|
+
# Returns true if this Condition has been negated, which means it will be prefixed with "NOT"
|
470
|
+
def negative?
|
471
|
+
@negative
|
472
|
+
end
|
473
|
+
|
474
|
+
# Gets the name of the table that this Condition refers to by taking it out of the
|
475
|
+
# association object.
|
476
|
+
def assign_join association = nil
|
477
|
+
@table_alias = association ? "#{association.aliased_table_name}." : ""
|
478
|
+
end
|
479
|
+
|
480
|
+
# Returns the full name of the column, including any assigned table alias.
|
481
|
+
def full_name
|
482
|
+
"#{@table_alias}#{name}"
|
483
|
+
end
|
484
|
+
|
485
|
+
# Generates the :condition parameter for this Condition, in ["sql", args] format.]
|
486
|
+
def to_find_conditions(join_association = {})
|
487
|
+
return nil if operator.nil?
|
488
|
+
|
489
|
+
op, arg_format, values = operator, "?", [operand]
|
490
|
+
op, arg_format, values = case operator
|
491
|
+
when :<=> then [ "BETWEEN", "? AND ?", [ operand.first, operand.last ] ]
|
492
|
+
when :=~ then
|
493
|
+
case operand
|
494
|
+
when String then [ "LIKE", arg_format, values ]
|
495
|
+
when Regexp then [ "REGEXP", arg_format, values.map(&:source) ]
|
496
|
+
end
|
497
|
+
when :==, :=== then
|
498
|
+
case operand
|
499
|
+
when Array then [ "IN", "(?)", values ]
|
500
|
+
when Range then [ "IN", "(?)", values ]
|
501
|
+
when Condition then [ "=", operand.full_name, [] ]
|
502
|
+
when nil then [ "IS", "NULL", [] ]
|
503
|
+
else [ "=", arg_format, values ]
|
504
|
+
end
|
505
|
+
when :contains then [ "LIKE", arg_format, values.map{|v| "%#{v}%" } ]
|
506
|
+
else
|
507
|
+
case operand
|
508
|
+
when Condition then [ op, oprand.full_name, [] ]
|
509
|
+
else [ op, arg_format, values ]
|
510
|
+
end
|
511
|
+
end
|
512
|
+
sql = "#{full_name} #{op} #{arg_format}"
|
513
|
+
sql = "NOT (#{sql})" if @negative
|
514
|
+
[ sql, *values ]
|
515
|
+
end
|
516
|
+
|
517
|
+
end
|
518
|
+
end
|
519
|
+
end
|
520
|
+
|
521
|
+
class << ActiveRecord::Base
|
522
|
+
include Squirrel::Hook
|
523
|
+
end
|
524
|
+
|
525
|
+
if defined?(ActiveRecord::NamedScope::Scope)
|
526
|
+
class ActiveRecord::NamedScope::Scope
|
527
|
+
include Squirrel::NamedScopeHook
|
528
|
+
end
|
529
|
+
end
|
530
|
+
|
531
|
+
[ ActiveRecord::Associations::HasManyAssociation,
|
532
|
+
ActiveRecord::Associations::HasAndBelongsToManyAssociation,
|
533
|
+
ActiveRecord::Associations::HasManyThroughAssociation
|
534
|
+
].each do |association_class|
|
535
|
+
association_class.send(:include, Squirrel::Hook)
|
536
|
+
end
|
537
|
+
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = %q{sinatra-squirrel}
|
5
|
+
s.version = "0.1.2"
|
6
|
+
|
7
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
8
|
+
s.authors = ["twilson63"]
|
9
|
+
s.date = %q{2009-06-17}
|
10
|
+
s.email = %q{tom@jackrussellsoftware.com}
|
11
|
+
s.extra_rdoc_files = [
|
12
|
+
"LICENSE",
|
13
|
+
"README.rdoc"
|
14
|
+
]
|
15
|
+
s.files = [
|
16
|
+
".document",
|
17
|
+
".gitignore",
|
18
|
+
"LICENSE",
|
19
|
+
"README.rdoc",
|
20
|
+
"Rakefile",
|
21
|
+
"VERSION.yml",
|
22
|
+
"lib/sinatra/extensions.rb",
|
23
|
+
"lib/sinatra/paginator.rb",
|
24
|
+
"lib/sinatra/squirrel.rb",
|
25
|
+
"sinatra-squirrel.gemspec",
|
26
|
+
"test/sinatra-squirrel_test.rb",
|
27
|
+
"test/test_helper.rb"
|
28
|
+
]
|
29
|
+
s.homepage = %q{http://github.com/twilson63/sinatra-squirrel}
|
30
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
31
|
+
s.require_paths = ["lib"]
|
32
|
+
s.rubygems_version = %q{1.3.4}
|
33
|
+
s.summary = %q{Port of Thought Bot Squirrel to Sinatra}
|
34
|
+
s.test_files = [
|
35
|
+
"test/test_helper.rb",
|
36
|
+
"test/sinatra-squirrel_test.rb"
|
37
|
+
]
|
38
|
+
|
39
|
+
if s.respond_to? :specification_version then
|
40
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
41
|
+
s.specification_version = 3
|
42
|
+
|
43
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
44
|
+
else
|
45
|
+
end
|
46
|
+
else
|
47
|
+
end
|
48
|
+
end
|
data/test/test_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: twilson63-sinatra-squirrel
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- twilson63
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-06-17 00:00:00 -07:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description:
|
17
|
+
email: tom@jackrussellsoftware.com
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files:
|
23
|
+
- LICENSE
|
24
|
+
- README.rdoc
|
25
|
+
files:
|
26
|
+
- .document
|
27
|
+
- .gitignore
|
28
|
+
- LICENSE
|
29
|
+
- README.rdoc
|
30
|
+
- Rakefile
|
31
|
+
- VERSION.yml
|
32
|
+
- lib/sinatra/extensions.rb
|
33
|
+
- lib/sinatra/paginator.rb
|
34
|
+
- lib/sinatra/squirrel.rb
|
35
|
+
- sinatra-squirrel.gemspec
|
36
|
+
- test/sinatra-squirrel_test.rb
|
37
|
+
- test/test_helper.rb
|
38
|
+
has_rdoc: false
|
39
|
+
homepage: http://github.com/twilson63/sinatra-squirrel
|
40
|
+
post_install_message:
|
41
|
+
rdoc_options:
|
42
|
+
- --charset=UTF-8
|
43
|
+
require_paths:
|
44
|
+
- lib
|
45
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - ">="
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: "0"
|
50
|
+
version:
|
51
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - ">="
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: "0"
|
56
|
+
version:
|
57
|
+
requirements: []
|
58
|
+
|
59
|
+
rubyforge_project:
|
60
|
+
rubygems_version: 1.2.0
|
61
|
+
signing_key:
|
62
|
+
specification_version: 3
|
63
|
+
summary: Port of Thought Bot Squirrel to Sinatra
|
64
|
+
test_files:
|
65
|
+
- test/test_helper.rb
|
66
|
+
- test/sinatra-squirrel_test.rb
|