aub-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.
Files changed (39) hide show
  1. data/README.rdoc +1 -1
  2. data/Rakefile +35 -1
  3. data/VERSION.yml +2 -2
  4. data/lib/record_filter/active_record.rb +55 -12
  5. data/lib/record_filter/conjunctions.rb +1 -1
  6. data/lib/record_filter/dsl/class_join.rb +1 -1
  7. data/lib/record_filter/dsl/conjunction.rb +1 -1
  8. data/lib/record_filter/dsl/conjunction_dsl.rb +244 -18
  9. data/lib/record_filter/dsl/dsl.rb +90 -25
  10. data/lib/record_filter/dsl/dsl_factory.rb +19 -0
  11. data/lib/record_filter/dsl/group_by.rb +1 -1
  12. data/lib/record_filter/dsl/join.rb +1 -1
  13. data/lib/record_filter/dsl/join_condition.rb +1 -1
  14. data/lib/record_filter/dsl/join_dsl.rb +36 -1
  15. data/lib/record_filter/dsl/limit.rb +1 -1
  16. data/lib/record_filter/dsl/named_filter.rb +1 -1
  17. data/lib/record_filter/dsl/order.rb +1 -1
  18. data/lib/record_filter/dsl/restriction.rb +218 -22
  19. data/lib/record_filter/dsl.rb +16 -1
  20. data/lib/record_filter/filter.rb +15 -21
  21. data/lib/record_filter/group_by.rb +1 -1
  22. data/lib/record_filter/join.rb +1 -1
  23. data/lib/record_filter/order.rb +1 -1
  24. data/lib/record_filter/query.rb +4 -5
  25. data/lib/record_filter/restrictions.rb +1 -1
  26. data/lib/record_filter/table.rb +27 -13
  27. data/lib/record_filter.rb +26 -5
  28. data/spec/active_record_spec.rb +144 -0
  29. data/spec/exception_spec.rb +41 -0
  30. data/spec/explicit_join_spec.rb +5 -4
  31. data/spec/limits_and_ordering_spec.rb +6 -5
  32. data/spec/models.rb +19 -0
  33. data/spec/named_filter_spec.rb +5 -2
  34. data/spec/proxying_spec.rb +26 -12
  35. data/spec/restrictions_spec.rb +40 -0
  36. data/spec/test.db +0 -0
  37. data/test/performance_test.rb +36 -0
  38. data/{lib/record_filter/join_table.rb → test/test.db} +0 -0
  39. metadata +8 -3
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
@@ -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,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
@@ -2,42 +2,107 @@ module RecordFilter
2
2
  module DSL
3
3
  class DSL < ConjunctionDSL
4
4
 
5
- SUBCLASSES = Hash.new do |h, k|
6
- h[k] = Class.new(DSL)
7
- end
8
-
9
- class << self
10
- def create(clazz)
11
- subclass(clazz).new(clazz, Conjunction.new(clazz, :all_of))
12
- end
13
-
14
- def subclass(clazz)
15
- SUBCLASSES[clazz.object_id]
16
- end
17
- end
18
-
19
- # This method can take two forms:
20
- # limit(offset, limit), or
21
- # limit(limit)
22
- def limit(offset_or_limit, limit=nil)
5
+ # Define an limit and/or offset for the results returned from the current
6
+ # filter. This method can only be called from the outermost scope of a filter
7
+ # (i.e. not inside of a having block, etc.). If it is called multiple times, the
8
+ # last one will override any others.
9
+ #
10
+ # ==== Parameters
11
+ # offset<Integer>::
12
+ # Used for the offset of the query.
13
+ # limit<Integer::
14
+ # Used as the limit for the query.
15
+ #
16
+ # ==== Returns
17
+ # nil
18
+ #
19
+ # ==== Alternatives
20
+ # If called with a single argument, it is assumed to represent the limit, and
21
+ # no offset will be specified.
22
+ #
23
+ # @public
24
+ def limit(offset, limit=nil)
23
25
  if limit
24
- @conjunction.add_limit(limit, offset_or_limit)
26
+ @conjunction.add_limit(limit, offset)
25
27
  else
26
- @conjunction.add_limit(offset_or_limit, nil)
28
+ @conjunction.add_limit(offset, nil)
27
29
  end
28
30
  nil
29
31
  end
30
32
 
31
- # This method can take two forms, as shown below.
32
- # order :permalink
33
- # order :permalink, :desc
34
- # order :photo => :path, :desc
35
- # order :photo => { :comment => :id }, :asc
33
+ # Define an order clause for the current query, with options for specifying
34
+ # both the column to use as well as the direction. This method can only be called
35
+ # in the outermost scope of a filter (i.e. not inside of a having block, etc.).
36
+ # Multiple calls will create multiple order clauses in the resulting query, and
37
+ # they will be added in the order in which they were called in the filter. In order
38
+ # to specify ordering on columns added through joins, a hash can be passed as the
39
+ # first argument, specifying a path through the joins to the column, as in this
40
+ # example:
41
+ #
42
+ # Blog.filter do
43
+ # having(:posts) do
44
+ # having(:comments).with(:created_at).greater_than(3.days.ago)
45
+ # end
46
+ # order(:posts => :comments => :created_at, :desc)
47
+ # order(:id, :asc)
48
+ # end
49
+ #
50
+ # ==== Parameters
51
+ # column<Symbol, Hash>::
52
+ # Specify the column for the ordering. If a symbol is given, it is assumed to represent
53
+ # a column in the class that is being filtered. With a hash argument, it is possible
54
+ # to specify a path to a column in one of the joined tables, as seen above.
55
+ # direction<Symbol>::
56
+ # Specifies the direction of the join. Should be either :asc or :desc.
57
+ #
58
+ # ==== Returns
59
+ # nil
60
+ #
61
+ # ==== Raises
62
+ # InvalidFilterException::
63
+ # If the direction is neither :asc nor :desc.
64
+ #
65
+ # ==== Alternatives
66
+ # As described above, it is possible to pass either a symbol or a hash as the first
67
+ # argument.
68
+ #
69
+ # @public
36
70
  def order(column, direction=:asc)
71
+ unless [:asc, :desc].include?(direction)
72
+ raise InvalidFilterException.new("The direction for orders must be either :asc or :desc but was #{direction}")
73
+ end
37
74
  @conjunction.add_order(column, direction)
38
75
  nil
39
76
  end
40
77
 
78
+ # Specify a group_by clause for the resulting query. This method can only be called
79
+ # in the outermost scope of a filter (i.e. not inside of a having block, etc.).
80
+ # Multiple calls will create multiple group_by clauses in the resulting query, and
81
+ # they will be added in the order in which they were called in the filter. In order
82
+ # to specify grouping on columns added through joins, a hash can be passed as the
83
+ # argument, specifying a path through the joins to the column, as in this example:
84
+ #
85
+ # Blog.filter do
86
+ # having(:posts) do
87
+ # having(:comments).with(:created_at).greater_than(3.days.ago)
88
+ # end
89
+ # group_by(:posts => :comments => :offensive)
90
+ # group_by(:id)
91
+ # end
92
+ #
93
+ # ==== Parameters
94
+ # column<Symbol, Hash>::
95
+ # If a symbol is specified, it is taken to represent the name of a column on the
96
+ # class being filtered. If a hash is given, it should represent a path through the
97
+ # joins to a column in one of the joined tables.
98
+ #
99
+ # ==== Returns
100
+ # nil
101
+ #
102
+ # ==== Alternatives
103
+ # As described above, it is possible to pass either a symbol or a hash as the argument.
104
+ #
105
+ # @public
41
106
  def group_by(column)
42
107
  @conjunction.add_group_by(column)
43
108
  nil
@@ -0,0 +1,19 @@
1
+ module RecordFilter
2
+ module DSL
3
+ class DSLFactory # :nodoc: all
4
+ SUBCLASSES = Hash.new do |h, k|
5
+ h[k] = Class.new(RecordFilter::DSL::DSL)
6
+ end
7
+
8
+ class << self
9
+ def create(clazz)
10
+ subclass(clazz).new(clazz, Conjunction.new(clazz, :all_of))
11
+ end
12
+
13
+ def subclass(clazz)
14
+ SUBCLASSES[clazz.object_id]
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -1,6 +1,6 @@
1
1
  module RecordFilter
2
2
  module DSL
3
- class GroupBy
3
+ class GroupBy # :nodoc: all
4
4
  attr_reader :column
5
5
 
6
6
  def initialize(column)