outoftime-record_filter 0.6.0 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -12,7 +12,7 @@ record_filter has the following top-level features:
12
12
 
13
13
  == Installation
14
14
 
15
- gem install outoftime-record_filter --source=http://gems.github.com
15
+ gem install outoftime-record_filter --source=http://gems.github.com
16
16
 
17
17
  == Usage
18
18
 
data/Rakefile CHANGED
@@ -1,4 +1,4 @@
1
- ENV['RUBYOPT'] = '-W1'
1
+ # ENV['RUBYOPT'] = '-W1'
2
2
 
3
3
  require 'rubygems'
4
4
  require 'rake'
@@ -22,3 +22,37 @@ rescue LoadError
22
22
  puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
23
23
  end
24
24
 
25
+ # Try to use hanna to create spiffier docs.
26
+ begin
27
+ require 'hanna/rdoctask'
28
+ rescue LoadError
29
+ require 'rake/rdoctask'
30
+ end
31
+
32
+ Rake::RDocTask.new do |rdoc|
33
+ if File.exist?('VERSION.yml')
34
+ config = YAML.load(File.read('VERSION.yml'))
35
+ version = "#{config[:major]}.#{config[:minor]}.#{config[:patch]}"
36
+ else
37
+ version = ""
38
+ end
39
+
40
+ rdoc.rdoc_dir = 'rdoc'
41
+ rdoc.title = "record_filter #{version}"
42
+ rdoc.rdoc_files.include('README*')
43
+ rdoc.rdoc_files.include('lib/**/*.rb')
44
+ end
45
+
46
+ begin
47
+ require 'ruby-prof/task'
48
+
49
+ RubyProf::ProfileTask.new do |t|
50
+ t.test_files = FileList['test/performance_test.rb']
51
+ t.output_dir = 'perf'
52
+ t.printer = :graph_html
53
+ t.min_percent = 5
54
+ end
55
+ rescue LoadError
56
+ puts 'Ruby-prof not available. Profiling tests are disabled.'
57
+ end
58
+
data/VERSION.yml CHANGED
@@ -1,4 +1,4 @@
1
1
  ---
2
- :patch: 0
3
2
  :major: 0
4
- :minor: 6
3
+ :minor: 8
4
+ :patch: 0
data/lib/record_filter.rb CHANGED
@@ -6,10 +6,31 @@ require 'active_record'
6
6
  require File.join(File.dirname(__FILE__), 'record_filter', file)
7
7
  end
8
8
 
9
+ # The base-level namespace for the record_filter code. See RecordFilter::ActiveRecordExtension::ClassMethods
10
+ # for a description of the public API.
9
11
  module RecordFilter
10
- AssociationNotFoundException = Class.new(StandardError)
11
- ColumnNotFoundException = Class.new(StandardError)
12
- InvalidFilterException = Class.new(StandardError)
13
- InvalidJoinException = Class.new(StandardError)
14
- NamedFilterNotFoundException = Class.new(StandardError)
12
+
13
+ # An exception that is raised when an implicit join is attempted on an association
14
+ # that does not exist.
15
+ class AssociationNotFoundException < StandardError; end
16
+
17
+ # An exception that is raised when attempting to place restrictions or specify an
18
+ # explicit join on a column that doesn't exist.
19
+ class ColumnNotFoundException < StandardError; end
20
+
21
+ # An exception that is raised when operations such as limit, order, group_by, or
22
+ # on are called out of context.
23
+ class InvalidFilterException < StandardError; end
24
+
25
+ # An exception that is raised when attempting to create a named filter with a name that
26
+ # already exists in the class it is created on or one of its superclasses.
27
+ class InvalidFilterNameException < StandardError; end
28
+
29
+ # An exception that is raised when no columns are privided to specify an explicit join.
30
+ class InvalidJoinException < StandardError; end
31
+
32
+ # An exception raised in the case where a named filter is called from within a filter
33
+ # and the named filter does not exist.
34
+ class NamedFilterNotFoundException < StandardError; end
15
35
  end
36
+
@@ -2,33 +2,68 @@ module RecordFilter
2
2
  # The ActiveRecordExtension module is mixed in to ActiveRecord::Base to form the
3
3
  # top-level API for interacting with record_filter. It adds public methods for
4
4
  # executing ad-hoc filters as well as for creating and querying named filters.
5
+ # See RecordFilter::ActiveRecordExtension::ClassMethods for more detail on the
6
+ # API.
5
7
  module ActiveRecordExtension
6
8
  module ClassMethods
7
9
 
8
- # Execute an ad-hoc filter
10
+ # Create a filter on the fly to find a set of results that matches the given criteria.
11
+ # This method, which can be called on any ActiveRecord::Base subclass, accepts a block
12
+ # that defines the contents of the filter and returns a Filter object that contains a list
13
+ # of the objects resulting from the query. See the documentation for RecordFilter::DSL
14
+ # for more information on how to specify the filter.
9
15
  #
10
16
  # ==== Parameters
11
- # block::
17
+ # block<Proc>::
12
18
  # A block that specifies the contents of the filter.
13
19
  #
14
20
  # ==== Returns
15
- # Filter:: The Filter object resulting from the query, which can be
16
- # treated as an array of the results.
21
+ # Filter::
22
+ # The Filter object resulting from the query, which can be treated as an array of the results.
17
23
  #
18
24
  # ==== Example
19
- # Blog.filter do
20
- # having(:posts).with(:name, nil)
21
- # end
22
- #—
25
+ # Blog.filter do
26
+ # having(:posts).with(:name, nil)
27
+ # end
28
+ #
23
29
  # @public
24
30
  def filter(&block)
25
31
  Filter.new(self, nil, &block)
26
32
  end
27
33
 
34
+ # Create a new named filter, which defines a function on the callee class that provides easy
35
+ # access to the query defined by the filter. Any number of named filters can be created on a
36
+ # class, and they can also be chained to create complex queries out of simple building blocks.
37
+ # In addition, named filters can accept any number of arguments in order to allow customization
38
+ # of their behavior when used. For more details on how to specify the contents of named filters,
39
+ # see the documentation for RecordFilter::DSL.
40
+ #
41
+ # Post.named_filter(:without_permalink) do
42
+ # with(:permalink, nil)
43
+ # end
44
+ #
45
+ # Post.named_filter(:created_after) do |time|
46
+ # with(:created_at).gt(time)
47
+ # end
48
+ #
49
+ # Post.without_permalink # :conditions => ['permalink IS NULL']
50
+ # Post.created_after(3.hours.ago) # :conditions => ['created_at > ?', 3.hours.ago]
51
+ # Post.without_permalink.created_after(3.hours.ago) # :conditions => ['permalink IS NULL AND created_at > ?', 3.hours.ago]
52
+ #
53
+ # ==== Raises
54
+ # InvalidFilterNameException::
55
+ # There is already a named filter with the given name on this class or one of its superclasses.
56
+ #
57
+ # ==== Returns
58
+ # nil
59
+ #
60
+ # @public
28
61
  def named_filter(name, &block)
29
- return if named_filters.include?(name.to_sym)
62
+ if named_filters.include?(name.to_sym)
63
+ raise InvalidFilterNameException.new("A named filter with the name #{name} already exists on the class #{self.name}.")
64
+ end
30
65
  local_named_filters << name.to_sym
31
- DSL::DSL::subclass(self).module_eval do
66
+ DSL::DSLFactory::subclass(self).module_eval do
32
67
  define_method(name, &block)
33
68
  end
34
69
 
@@ -37,8 +72,16 @@ module RecordFilter
37
72
  Filter.new(self, name, *args)
38
73
  end
39
74
  end
75
+ nil
40
76
  end
41
77
 
78
+ # Retreive a list of named filters that apply to a specific class, including ones
79
+ # that were defined in its superclasses.
80
+ #
81
+ # ==== Returns
82
+ # Array:: A list of the names of the named filters, as symbols.
83
+ #
84
+ # @public
42
85
  def named_filters
43
86
  result = local_named_filters.dup
44
87
  result.concat(superclass.named_filters) if (superclass && superclass.respond_to?(:named_filters))
@@ -46,8 +89,8 @@ module RecordFilter
46
89
  end
47
90
 
48
91
  protected
49
-
50
- def local_named_filters
92
+
93
+ def local_named_filters # :nodoc:
51
94
  @local_named_filters ||= []
52
95
  end
53
96
  end
@@ -1,5 +1,5 @@
1
1
  module RecordFilter
2
- module Conjunctions
2
+ module Conjunctions # :nodoc: all
3
3
  class Base
4
4
  attr_reader :table_name, :limit, :offset
5
5
 
@@ -1,6 +1,21 @@
1
- %w(class_join conjunction conjunction_dsl dsl group_by join join_dsl join_condition limit named_filter order restriction).each { |file| require File.join(File.dirname(__FILE__), 'dsl', file) }
1
+ %w(class_join conjunction conjunction_dsl dsl dsl_factory group_by join join_dsl join_condition limit named_filter order restriction).each { |file| require File.join(File.dirname(__FILE__), 'dsl', file) }
2
2
 
3
3
  module RecordFilter
4
+ # The DSL module defines the structure of the criteria API used in calls to
5
+ # filter and named_filter. The API is defined by its four submodules, which
6
+ # define context-specific hooks for the API as well as defining a list of
7
+ # available restrictions. At the top level of a filter definition, all of the
8
+ # methods in DSL and ConjunctionDSL are available. In inner blocks, the methods
9
+ # in ConjunctionDSL are available, and within explicit joins defined using 'join'
10
+ # the methods in JoinDSL are added. The API provides access to:
11
+ #
12
+ # * Restrictions, using ConjunctionDSL.with and the methods in Restriction.
13
+ # * Conjunctions, such as all_of, any_of, etc. in ConjunctionDSL.
14
+ # * Implicit joins on associations, using ConjunctionDSL.having
15
+ # * Explicit joins, using ConjunctionDSL.join and JoinDSL.on
16
+ # * Ordering, using DSL.order
17
+ # * Grouping, using DSL.group_by
18
+ # * Limits and offsets, using DSL.limit
4
19
  module DSL
5
20
  end
6
21
  end
@@ -1,6 +1,6 @@
1
1
  module RecordFilter
2
2
  module DSL
3
- class ClassJoin
3
+ class ClassJoin # :nodoc: all
4
4
  attr_reader :join_class, :join_type, :table_alias, :conjunction
5
5
 
6
6
  def initialize(join_class, join_type, table_alias, conjunction, join_conditions)
@@ -1,6 +1,6 @@
1
1
  module RecordFilter
2
2
  module DSL
3
- class Conjunction
3
+ class Conjunction # :nodoc: all
4
4
 
5
5
  attr_reader :type, :steps
6
6
 
@@ -1,78 +1,304 @@
1
1
  module RecordFilter
2
2
  module DSL
3
+ # The ConjunctionDSL is used for specifying restrictions, conjunctions and joins, with methods that
4
+ # can be accessed from any point in a filter declaration. The where method is used for creating
5
+ # restrictions, conjunctions are specified through any_of, all_of, none_of and not_all_of, and joins
6
+ # are described by having and join.
3
7
  class ConjunctionDSL
4
8
 
5
- attr_reader :conjunction
9
+ attr_reader :conjunction # :nodoc:
6
10
 
7
- def initialize(model_class, conjunction)
11
+ def initialize(model_class, conjunction) # :nodoc:
8
12
  @model_class = model_class
9
13
  @conjunction = conjunction
10
14
  end
11
15
 
12
- # restriction
16
+ # Specify a condition on the given column, which will be added to the WHERE clause
17
+ # of the resulting query. This method returns a Restriction object, which can be called
18
+ # with any of the specific restriction methods described there in order to create many
19
+ # types of conditions. If both a column name and a value are passed, this will automatically
20
+ # create an equality condition, so the following two examples are equal:
21
+ # with(:permalink, 'junk')
22
+ # with(:permalink).equal_to('junk')
23
+ # If nil is passed as the second argument, an is_null restriction will automatically be
24
+ # created, so these two examples are equal as well:
25
+ # with(:permalink, nil)
26
+ # with(:permalink).is_null
27
+ # This method can be called at any point in the filter specification, and the appropriate
28
+ # clauses will be created if it is called within or other conjunctions.
29
+ #
30
+ # ==== Parameters
31
+ # column<Symbol>::
32
+ # The name of the column to restrict. The column is assumed to exist in the table that is
33
+ # currently in scope. In the outer block of a filter, that would be the table being filtered,
34
+ # and within joins it would be the table being joined.
35
+ # value<value, optional>::
36
+ # If specified, the value will be used to automatically create either an equality restriction
37
+ # or an IS NULL test, as described above.
38
+ #
39
+ # ==== Returns
40
+ # Restriction::
41
+ # A restriction object that can be used to create a specific condition. See the API in
42
+ # Restriction for options.
43
+ #
44
+ # ==== Alternatives
45
+ # The value parameter is optional, as described above.
46
+ #
47
+ # @public
13
48
  def with(column, value=Restriction::DEFAULT_VALUE)
14
49
  return @conjunction.add_restriction(column, value)
15
50
  end
16
51
 
17
- # conjunction
52
+ # Add a where clause that will pass if any of the conditions specified within it
53
+ # are true. Any restrictions created inside the given block are OR'ed together
54
+ # in the final query, and the block can contain any number of joins, restrictions, or
55
+ # other conjunctions.
56
+ # Blog.filter do
57
+ # any_of do
58
+ # with(:created_at, nil)
59
+ # with(:created_at).greater_than(3.days.ago)
60
+ # end
61
+ # end
62
+ #
63
+ # # :conditions => { ['blogs.created_at IS NULL OR blogs.created_at > ?', 3.days.ago] }
64
+ #
65
+ # ==== Parameters
66
+ # block<Proc>::
67
+ # The block can contain any sequence of calls, and the conditions that it contains will be
68
+ # OR'ed together to create a where clause.
69
+ #
70
+ # ==== Returns
71
+ # nil
72
+ #
73
+ # @public
18
74
  def any_of(&block)
19
75
  @conjunction.add_conjunction(:any_of, &block)
20
76
  nil
21
77
  end
22
78
 
23
- # conjunction
79
+ # Add a where clause that will pass only if all of the conditions specified within it
80
+ # are true. Any restrictions created inside the given block are AND'ed together
81
+ # in the final query, and the block can contain any number of joins, restrictions, or
82
+ # other conjunctions.
83
+ # Blog.filter do
84
+ # all_of do
85
+ # with(:created_at, nil)
86
+ # with(:created_at).greater_than(3.days.ago)
87
+ # end
88
+ # end
89
+ #
90
+ # # :conditions => { ['blogs.created_at IS NULL AND blogs.created_at > ?', 3.days.ago] }
91
+ #
92
+ # ==== Parameters
93
+ # block<Proc>::
94
+ # The block can contain any sequence of calls, and the conditions that it contains will be
95
+ # AND'ed together to create a where clause.
96
+ #
97
+ # ==== Returns
98
+ # nil
99
+ #
100
+ # @public
24
101
  def all_of(&block)
25
102
  @conjunction.add_conjunction(:all_of, &block)
26
103
  nil
27
104
  end
28
105
 
106
+ # Add a where clause that will pass only if none of the conditions specified within it
107
+ # are true. Any restrictions created inside the given block are OR'ed together
108
+ # in the final query and the result is negated. The block can contain any number of joins,
109
+ # restrictions, or other conjunctions.
110
+ # Blog.filter do
111
+ # none_of do
112
+ # with(:created_at, nil)
113
+ # with(:created_at).greater_than(3.days.ago)
114
+ # end
115
+ # end
116
+ #
117
+ # # :conditions => { ['NOT (blogs.created_at IS NULL OR blogs.created_at > ?)', 3.days.ago] }
118
+ #
119
+ # ==== Parameters
120
+ # block<Proc>::
121
+ # The block can contain any sequence of calls, and the conditions that it contains will be
122
+ # OR'ed together and then negated to create a where clause.
123
+ #
124
+ # ==== Returns
125
+ # nil
126
+ #
127
+ # @public
29
128
  def none_of(&block)
30
129
  @conjunction.add_conjunction(:none_of, &block)
31
130
  nil
32
131
  end
33
132
 
133
+ # Add a where clause that will pass unless all of the conditions specified within it
134
+ # are true. Any restrictions created inside the given block are AND'ed together
135
+ # in the final query and the result is negated. The block can contain any number of joins,
136
+ # restrictions, or other conjunctions.
137
+ # Blog.filter do
138
+ # none_of do
139
+ # with(:created_at, nil)
140
+ # with(:created_at).greater_than(3.days.ago)
141
+ # end
142
+ # end
143
+ #
144
+ # # :conditions => { ['NOT (blogs.created_at IS NULL AND blogs.created_at > ?)', 3.days.ago] }
145
+ #
146
+ # ==== Parameters
147
+ # block<Proc>::
148
+ # The block can contain any sequence of calls, and the conditions that it contains will be
149
+ # AND'ed together and then negated to create a where clause.
150
+ #
151
+ # ==== Returns
152
+ # nil
153
+ #
154
+ # @public
34
155
  def not_all_of(&block)
35
156
  @conjunction.add_conjunction(:not_all_of, &block)
36
157
  nil
37
158
  end
38
159
 
39
- # join
40
- def having(join_type_or_association, association=nil, &block)
160
+ # Create an implicit join using an association as the target. This method allows you to
161
+ # easily specify a join without specifying the columns to use by taking any needed data
162
+ # from the specified ActiveRecord association. If given, the block will be evaluated in
163
+ # the context of the table that has been joined, so any restrictions or other joins will
164
+ # be performed using its columns and associations. For example, if a Post has_many comments
165
+ # then the following code will join to the comments table and restrict the comments based
166
+ # on their created_at field:
167
+ # Post.filter do
168
+ # having(:comments) do
169
+ # with(:created_at).greater_than(3.days.ago)
170
+ # end
171
+ # end
172
+ # If one argument is given, it is assumed to be a symbol representing the name of the association
173
+ # that will be used for the join and a join type of :inner will be used. If two arguments are
174
+ # provided, the first one is assumed to be the join type, which can be one of :inner, :left or
175
+ # :right and the second one is the association name. An alias will automatically be created
176
+ # for the joined table named "#{left_table}__#{association_name}", so in the above example, the
177
+ # alias would be posts__comments.
178
+ #
179
+ # ==== Parameters
180
+ # join_type<Symbol>::
181
+ # Specifies the type of join to perform, and can be one of :inner, :left or :right. :left
182
+ # and :right will create left and right outer joins, respectively.
183
+ # association<Symbol>::
184
+ # The name of the association to use as a base for the join.
185
+ #
186
+ # ==== Returns
187
+ # ConjunctionDSL::
188
+ # A DSL object is returned in order to allow constructs like: having(:comments).with(:offensive, true)
189
+ #
190
+ # ==== Alternatives
191
+ # If only one argument is given, the join type will default to :inner and the first argument will
192
+ # be used as the association name.
193
+ #
194
+ # @public
195
+ def having(join_type, association=nil, &block)
41
196
  if association.nil?
42
- association, join_type = join_type_or_association, nil
43
- else
44
- join_type = join_type_or_association
197
+ association, join_type = join_type, nil
45
198
  end
46
199
  @conjunction.add_join(association, join_type, &block)
47
200
  end
48
201
 
202
+ # Create an explicit join on the table of the given class. This method allows more complex
203
+ # joins to be speficied than can be created using having, including jump joins and ones that
204
+ # include conditions on column values. The method accepts a block that can contain any sequence
205
+ # of conjunctions, restrictions, or other joins, but it must also contain at least one call to
206
+ # JoinDSL.on to specify the conditions for the join.
207
+ #
208
+ # ==== Parameters
209
+ # clazz<Class>::
210
+ # The class that is being joined to.
211
+ # join_type<Symbol>::
212
+ # Indicates the type of join to use and must be one of :inner, :left or :right, where :left
213
+ # or :right will create a LEFT or RIGHT OUTER join respectively.
214
+ # table_alias<String, optional>::
215
+ # If provided, will specify an alias to use in the SQL when referring to the joined table.
216
+ # If the argument is not given, the alias will be "#{left_table}__#{clazz.name}"
217
+ # block<Proc>
218
+ # The contents of the join block can contain any sequence of conjunctions, restrictions, or joins.
219
+ #
220
+ # ==== Returns
221
+ # JoinDSL::
222
+ # A DSL object that can be used to specify the contents of the join. Returning this value allows
223
+ # for constructions like: join(Comment, :inner).on(:id => :post_id)
224
+ #
225
+ # @public
49
226
  def join(clazz, join_type, table_alias=nil, &block)
50
227
  @conjunction.add_class_join(clazz, join_type, table_alias, &block)
51
228
  end
52
229
 
230
+ # Access the class that the current filter is being applied to. This is necessary
231
+ # because the filter is evaluated in the context of the DSL object, so self will
232
+ # not give access to any methods that need to be called on the filtered class.
233
+ # It is especially useful in named filters that may be defined in a way that allows
234
+ # them to apply to multiple classes.
235
+ #
236
+ # ==== Returns
237
+ # Class::
238
+ # The class that is currently being filtered.
239
+ #
240
+ # @public
53
241
  def filter_class
54
242
  @model_class
55
243
  end
244
+
245
+ # Enable calling of named filters from within other filters by catching unknown calls
246
+ # and assuming that they are to named filters. This enables the following examples:
247
+ # class Post < ActiveRecord::Base
248
+ # has_many :comments
249
+ # named_filter(:empty) { with(:contents).nil }
250
+ # end
251
+ #
252
+ # class Comment < ActiveRecord::Base
253
+ # belongs_to :post
254
+ # named_filter(:offensive) { |value| with(:offensive, value) }
255
+ # end
256
+ #
257
+ # Post.filter do
258
+ # with(:created_at).less_than(1.hour.ago)
259
+ # empty
260
+ # end
261
+ #
262
+ # # Results in:
263
+ # # :conditions => { ['posts.created_at < ? AND posts.contents IS NULL', 1.hour.ago] }
264
+ # # And even cooler:
265
+ #
266
+ # Post.filter do
267
+ # having(:comments).offensive(true)
268
+ # end
269
+ #
270
+ # # Results in:
271
+ # # :conditions => { ['posts__comments.offensive = ?', true] }
272
+ # # :joins => { 'INNER JOIN "comments" AS posts__comments ON "posts".id = posts__comments.post_id' }
273
+ #
274
+ # ==== Parameters
275
+ # args<Array>::
276
+ # The arguments to pass to the named filter when called.
277
+ #
278
+ # @public
279
+ def method_missing(method, *args)
280
+ @conjunction.add_named_filter(method, *args)
281
+ end
56
282
 
57
- def limit(offset_or_limit, limit=nil)
283
+ #
284
+ # Define these_methods here just so that we can throw exceptions when they are called. They should not
285
+ # be callable in the scope of a conjunction_dsl.
286
+ #
287
+ def limit(offset_or_limit, limit=nil) # :nodoc:
58
288
  raise InvalidFilterException.new('Calls to limit can only be made in the outer block of a filter.')
59
289
  end
60
290
 
61
- def order(column, direction=:asc)
291
+ def order(column, direction=:asc) # :nodoc:
62
292
  raise InvalidFilterException.new('Calls to order can only be made in the outer block of a filter.')
63
293
  end
64
294
 
65
- def group_by(column)
295
+ def group_by(column) # :nodoc:
66
296
  raise InvalidFilterException.new('Calls to group_by can only be made in the outer block of a filter.')
67
297
  end
68
298
 
69
- def on(column, value=Restriction::DEFAULT_VALUE)
299
+ def on(column, value=Restriction::DEFAULT_VALUE) # :nodoc:
70
300
  raise InvalidFilterException.new('Calls to on can only be made in the block of a call to join.')
71
301
  end
72
-
73
- def method_missing(method, *args)
74
- @conjunction.add_named_filter(method, *args)
75
- end
76
302
  end
77
303
  end
78
304
  end