twilson63-sinatra-squirrel 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
@@ -0,0 +1,5 @@
1
+ *.sw?
2
+ .DS_Store
3
+ coverage
4
+ rdoc
5
+ pkg
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.
@@ -0,0 +1,9 @@
1
+ = sinatra-squirrel
2
+
3
+ Port of the Thought Bots Squirrel Search to Sinatra
4
+
5
+ Trying to get the release to build the gem
6
+
7
+ == Copyright
8
+
9
+ Copyright (c) 2009 twilson63. See LICENSE for details.
@@ -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
+
@@ -0,0 +1,4 @@
1
+ ---
2
+ :major: 0
3
+ :minor: 1
4
+ :patch: 2
@@ -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
@@ -0,0 +1,7 @@
1
+ require 'test_helper'
2
+
3
+ class SinatraSquirrelTest < Test::Unit::TestCase
4
+ should "probably rename this file and start testing for real" do
5
+ flunk "hey buddy, you should probably rename this file and start testing for real"
6
+ end
7
+ end
@@ -0,0 +1,10 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'shoulda'
4
+
5
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
6
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
7
+ require 'sinatra-squirrel'
8
+
9
+ class Test::Unit::TestCase
10
+ end
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