twilson63-sinatra-squirrel 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|