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.
@@ -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