Class: Squeel::Visitors::Visitor

Inherits:
Object
  • Object
show all
Defined in:
lib/squeel/visitors/visitor.rb

Overview

The Base visitor class, containing the default behavior common to subclasses.

Constant Summary collapse

DISPATCH =

A hash that caches the method name to use for a visitor for a given class

Hash.new do |hash, klass|
  hash[klass] = "visit_#{(klass.name || '').gsub('::', '_')}"
end

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(context = nil) ⇒ Visitor

Create a new Visitor that uses the supplied context object to contextualize visited nodes.

Parameters:

  • context (Context) (defaults to: nil)

    The context to use for node visitation.



15
16
17
18
# File 'lib/squeel/visitors/visitor.rb', line 15

def initialize(context = nil)
  @context = context
  @hash_context_depth = 0
end

Instance Attribute Details

#contextObject

Returns the value of attribute context.



8
9
10
# File 'lib/squeel/visitors/visitor.rb', line 8

def context
  @context
end

Class Method Details

.can_visit?(object) ⇒ Boolean

Returns Whether or not visitors of this class can visit the given object.

Parameters:

  • object

    The object to check

Returns:

  • (Boolean)

    Whether or not visitors of this class can visit the given object



38
39
40
41
42
43
44
45
# File 'lib/squeel/visitors/visitor.rb', line 38

def self.can_visit?(object)
  @can_visit ||= Hash.new do |hash, klass|
    hash[klass] = klass.ancestors.detect { |ancestor|
      private_method_defined? DISPATCH[ancestor]
    } ? true : false
  end
  @can_visit[object.class]
end

Instance Method Details

#accept(object, parent = context.base) ⇒ Object

Accept an object.

Parameters:

  • object

    The object to visit

  • parent (defaults to: context.base)

    The parent of this object, to track the object’s place in any association hierarchy.

Returns:

  • The results of the node visitation, typically an Arel object of some kind.



26
27
28
# File 'lib/squeel/visitors/visitor.rb', line 26

def accept(object, parent = context.base)
  visit(object, parent)
end

#can_visit?(object) ⇒ Boolean

Returns Whether or not the visitor can visit the given object.

Parameters:

  • object

    The object to check

Returns:

  • (Boolean)

    Whether or not the visitor can visit the given object



32
33
34
# File 'lib/squeel/visitors/visitor.rb', line 32

def can_visit?(object)
  self.class.can_visit? object
end

#hash_context_shifted?Boolean (private)

If we’re visiting stuff in a hash, it’s good to check whether or not we’ve shifted context already. If we have, we may want to use caution as it pertains to certain input, in case it’s untrusted. See CVE-2012-2661 for info.

Returns:

  • (Boolean)

    Whether we’re within a new context.



69
70
71
# File 'lib/squeel/visitors/visitor.rb', line 69

def hash_context_shifted?
  @hash_context_depth > 0
end

#implies_hash_context_shift?(v) ⇒ Boolean (private)

Returns Whether the given value implies a context change.

Parameters:

  • v

    The value to consider

Returns:

  • (Boolean)

    Whether the given value implies a context change



75
76
77
# File 'lib/squeel/visitors/visitor.rb', line 75

def implies_hash_context_shift?(v)
  can_visit?(v)
end

#quote(value) ⇒ Arel::Nodes::SqlLiteral (private)

Quote a value based on its type, not on the last column used by the Arel visitor. This is occasionally necessary to avoid having Arel quote a value according to an integer column, converting ‘My String’ to 0.

Parameters:

  • value

    The value to quote

Returns:

  • (Arel::Nodes::SqlLiteral)

    if the value needs to be pre-quoted

  • the unquoted value, if default quoting won’t hurt.



137
138
139
140
141
142
143
144
145
146
147
148
149
150
# File 'lib/squeel/visitors/visitor.rb', line 137

def quote(value)
  if quoted? value
    case value
    when Array
      value.map {|v| quote(v)}
    when Range
      Range.new(quote(value.begin), quote(value.end), value.exclude_end?)
    else
      Arel.sql(arel_visitor.accept value)
    end
  else
    value
  end
end

#quoted?(object) ⇒ Boolean (private)

Important to avoid accidentally allowing the default Arel visitor’s last_column quoting behavior (where a value is quoted as though it is of the type of the last visited column). This can wreak havoc with Functions and Operations.

Parameters:

  • object

    The object to check

Returns:

  • (Boolean)

    Whether or not the Arel visitor will try to quote the object if not passed as an SqlLiteral.



120
121
122
123
124
125
126
127
128
# File 'lib/squeel/visitors/visitor.rb', line 120

def quoted?(object)
  case object
  when NilClass, Arel::Nodes::SqlLiteral, Bignum, Fixnum,
    Arel::SelectManager
    false
  else
    true
  end
end

#symbolify(o) ⇒ Object (private)



49
50
51
52
53
54
55
56
# File 'lib/squeel/visitors/visitor.rb', line 49

def symbolify(o)
  case o
  when Symbol, String, Nodes::Stub
    o.to_sym
  else
    nil
  end
end

#visit(object, parent) ⇒ Object (private)

Visit the object.

Parameters:

  • object

    The object to visit

  • parent

    The object’s parent within the context



156
157
158
159
160
161
162
163
164
165
166
167
# File 'lib/squeel/visitors/visitor.rb', line 156

def visit(object, parent)
  send(DISPATCH[object.class], object, parent)
rescue NoMethodError => e
  raise e if respond_to?(DISPATCH[object.class], true)

  superklass = object.class.ancestors.find { |klass|
    respond_to?(DISPATCH[klass], true)
  }
  raise(TypeError, "Cannot visit #{object.class}") unless superklass
  DISPATCH[object.class] = DISPATCH[superklass]
  retry
end

#visit_ActiveRecord_Base(o, parent) ⇒ Fixnum (private)

Visit ActiveRecord::Base objects. These should be converted to their id before being used in a comparison.

Parameters:

  • o (ActiveRecord::Base)

    The AR::Base object to visit

  • parent

    The current parent object in the context

Returns:

  • (Fixnum)

    The id of the object



380
381
382
# File 'lib/squeel/visitors/visitor.rb', line 380

def visit_ActiveRecord_Base(o, parent)
  o.id
end

#visit_ActiveRecord_Relation(o, parent) ⇒ Arel::SelectManager (private)

Visit an Active Record Relation, returning an Arel::SelectManager

Parameters:

  • o (ActiveRecord::Relation)

    The Relation to visit

  • parent

    The parent object in the context

Returns:

  • (Arel::SelectManager)

    The Arel select manager that represents the relation’s query



370
371
372
# File 'lib/squeel/visitors/visitor.rb', line 370

def visit_ActiveRecord_Relation(o, parent)
  o.arel
end

#visit_Array(o, parent) ⇒ Array (private)

Visit an array, which involves accepting any values we know how to accept, and skipping the rest.

Parameters:

  • o (Array)

    The Array to visit

  • parent

    The current parent object in the context

Returns:

  • (Array)

    The visited array



188
189
190
# File 'lib/squeel/visitors/visitor.rb', line 188

def visit_Array(o, parent)
  o.map { |v| can_visit?(v) ? visit(v, parent) : v }.flatten
end

#visit_Hash(o, parent) ⇒ Array (private)

Visit a Hash. This entails iterating through each key and value and visiting each value in turn.

Parameters:

  • o (Hash)

    The Hash to visit

  • parent

    The current parent object in the context

Returns:

  • (Array)

    An array of values for use in an ordering, grouping, etc.



198
199
200
201
202
203
204
205
206
# File 'lib/squeel/visitors/visitor.rb', line 198

def visit_Hash(o, parent)
  o.map do |k, v|
    if implies_hash_context_shift?(v)
      visit_with_hash_context_shift(k, v, parent)
    else
      visit_without_hash_context_shift(k, v, parent)
    end
  end.flatten
end

#visit_passthrough(object, parent) ⇒ Object (private) Also known as: visit_Fixnum, visit_Bignum

Pass an object through the visitor unmodified. This is in order to allow objects that don’t require modification to be handled by Arel directly.

Parameters:

  • object

    The object to visit

  • parent

    The object’s parent within the context

Returns:

  • The object, unmodified



176
177
178
# File 'lib/squeel/visitors/visitor.rb', line 176

def visit_passthrough(object, parent)
  object
end

#visit_Squeel_Nodes_And(o, parent) ⇒ Arel::Nodes::Grouping (private)

Visit a Squeel And node, returning an Arel Grouping containing an Arel And node.

Parameters:

  • o (Nodes::And)

    The And node to visit

  • parent

    The parent object in the context

Returns:

  • (Arel::Nodes::Grouping)

    A grouping node, containnig an Arel And node as its expression. All children will be visited before being passed to the And.



276
277
278
# File 'lib/squeel/visitors/visitor.rb', line 276

def visit_Squeel_Nodes_And(o, parent)
  Arel::Nodes::Grouping.new(Arel::Nodes::And.new(visit(o.children, parent)))
end

#visit_Squeel_Nodes_As(o, parent) ⇒ Arel::Nodes::As (private)

Visit a Squeel As node, resulting in am Arel As node.

Parameters:

  • The (Nodes::As)

    As node to visit

  • parent

    The parent object in the context

Returns:

  • (Arel::Nodes::As)

    The resulting as node.



257
258
259
260
261
262
263
264
265
266
# File 'lib/squeel/visitors/visitor.rb', line 257

def visit_Squeel_Nodes_As(o, parent)
  left = visit(o.left, parent)
  # Some nodes, like Arel::SelectManager, have their own #as methods,
  # with behavior that we don't want to clobber.
  if left.respond_to?(:as)
    left.as(o.right)
  else
    Arel::Nodes::As.new(left, o.right)
  end
end

#visit_Squeel_Nodes_Function(o, parent) ⇒ Arel::Nodes::NamedFunction (private)

Visit a Squeel function, returning an Arel NamedFunction node.

Parameters:

  • o (Nodes::Function)

    The function node to visit

  • parent

    The parent object in the context

Returns:

  • (Arel::Nodes::NamedFunction)

    A named function node. Function arguments are visited, if necessary, before being passed to the NamedFunction.



313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
# File 'lib/squeel/visitors/visitor.rb', line 313

def visit_Squeel_Nodes_Function(o, parent)
  args = o.args.map do |arg|
    case arg
    when Nodes::Function, Nodes::As, Nodes::Literal, Nodes::Grouping, Nodes::KeyPath, Nodes::KeyPath
      visit(arg, parent)
    when ActiveRecord::Relation
      arg.arel.ast
    when Symbol, Nodes::Stub
      Arel.sql(arel_visitor.accept contextualize(parent)[arg.to_s])
    else
      quote arg
    end
  end

  Arel::Nodes::NamedFunction.new(o.function_name, args)
end

#visit_Squeel_Nodes_Grouping(o, parent) ⇒ Arel::Nodes::Grouping (private)

Visit a Squeel Grouping node, returning an Arel Grouping node.

Parameters:

  • o (Nodes::Grouping)

    The Grouping node to visit

  • parent

    The parent object in the context

Returns:

  • (Arel::Nodes::Grouping)

    An Arel Grouping node, with expression visited



303
304
305
# File 'lib/squeel/visitors/visitor.rb', line 303

def visit_Squeel_Nodes_Grouping(o, parent)
  Arel::Nodes::Grouping.new(visit(o.expr, parent))
end

#visit_Squeel_Nodes_KeyPath(o, parent) ⇒ Object (private)

Visit a keypath. This will traverse the keypath’s “path”, setting a new parent as though the keypath’s endpoint was in a deeply-nested hash, then visit the endpoint with the new parent.

Parameters:

  • o (Nodes::KeyPath)

    The keypath to visit

  • parent

    The keypath’s parent within the context

Returns:

  • The visited endpoint, with the parent from the KeyPath’s path.



237
238
239
240
241
# File 'lib/squeel/visitors/visitor.rb', line 237

def visit_Squeel_Nodes_KeyPath(o, parent)
  parent = traverse(o, parent)

  visit(o.endpoint, parent)
end

#visit_Squeel_Nodes_Literal(o, parent) ⇒ Arel::Nodes::SqlLiteral (private)

Visit a Literal by converting it to an Arel SqlLiteral

Parameters:

  • o (Nodes::Literal)

    The Literal to visit

  • parent

    The parent object in the context (unused)

Returns:

  • (Arel::Nodes::SqlLiteral)

    An SqlLiteral



248
249
250
# File 'lib/squeel/visitors/visitor.rb', line 248

def visit_Squeel_Nodes_Literal(o, parent)
  Arel.sql(o.expr)
end

#visit_Squeel_Nodes_Not(o, parent) ⇒ Arel::Nodes::Not (private)

Visit a Squeel Not node, returning an Arel Not node.

Parameters:

  • o (Nodes::Not)

    The Not node to visit

  • parent

    The parent object in the context

Returns:

  • (Arel::Nodes::Not)

    An Arel Not node, with expression visited



294
295
296
# File 'lib/squeel/visitors/visitor.rb', line 294

def visit_Squeel_Nodes_Not(o, parent)
  Arel::Nodes::Not.new(visit(o.expr, parent))
end

#visit_Squeel_Nodes_Operation(o, parent) ⇒ Arel::Nodes::InfixOperation (private)

Visit a Squeel operation node, convering it to an Arel InfixOperation (or subclass, as appropriate)

Parameters:

  • o (Nodes::Operation)

    The Operation node to visit

  • parent

    The parent object in the context

Returns:

  • (Arel::Nodes::InfixOperation)

    The InfixOperation (or Addition, Multiplication, etc) node, with both operands visited, if needed.



337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
# File 'lib/squeel/visitors/visitor.rb', line 337

def visit_Squeel_Nodes_Operation(o, parent)
  args = o.args.map do |arg|
    case arg
    when Nodes::Function, Nodes::As, Nodes::Literal, Nodes::Grouping, Nodes::KeyPath
      visit(arg, parent)
    when Symbol, Nodes::Stub
      Arel.sql(arel_visitor.accept contextualize(parent)[arg.to_s])
    else
      quote arg
    end
  end

  op = case o.operator
  when :+
    Arel::Nodes::Addition.new(args[0], args[1])
  when :-
    Arel::Nodes::Subtraction.new(args[0], args[1])
  when :*
    Arel::Nodes::Multiplication.new(args[0], args[1])
  when :/
    Arel::Nodes::Division.new(args[0], args[1])
  else
    Arel::Nodes::InfixOperation.new(o.operator, args[0], args[1])
  end

  op
end

#visit_Squeel_Nodes_Or(o, parent) ⇒ Arel::Nodes::Or (private)

Visit a Squeel Or node, returning an Arel Or node.

Parameters:

  • o (Nodes::Or)

    The Or node to visit

  • parent

    The parent object in the context

Returns:

  • (Arel::Nodes::Or)

    An Arel Or node, with left and right sides visited



285
286
287
# File 'lib/squeel/visitors/visitor.rb', line 285

def visit_Squeel_Nodes_Or(o, parent)
  Arel::Nodes::Grouping.new(Arel::Nodes::Or.new(visit(o.left, parent), (visit(o.right, parent))))
end

#visit_Squeel_Nodes_Stub(o, parent) ⇒ Arel::Attribute (private)

Visit a stub. This will return an attribute named after the stub against the current parent’s contextualized table.

Parameters:

  • o (Nodes::Stub)

    The stub to visit

  • parent

    The stub’s parent within the context

Returns:

  • (Arel::Attribute)

    An attribute on the contextualized parent table



226
227
228
# File 'lib/squeel/visitors/visitor.rb', line 226

def visit_Squeel_Nodes_Stub(o, parent)
  contextualize(parent)[o.to_s]
end

#visit_Symbol(o, parent) ⇒ Arel::Attribute (private)

Visit a symbol. This will return an attribute named after the symbol against the current parent’s contextualized table.

Parameters:

  • o (Symbol)

    The symbol to visit

  • parent

    The symbol’s parent within the context

Returns:

  • (Arel::Attribute)

    An attribute on the contextualized parent table



215
216
217
# File 'lib/squeel/visitors/visitor.rb', line 215

def visit_Symbol(o, parent)
  contextualize(parent)[o]
end

#visit_with_hash_context_shift(k, v, parent) ⇒ Object (private)

Change context (by setting the new parent to the result of a #find or #traverse on the key), then accept the given value.

Parameters:

  • k

    The hash key

  • v

    The hash value

  • parent

    The current parent object in the context

Returns:

  • The visited value



86
87
88
89
90
91
92
93
94
95
96
97
98
99
# File 'lib/squeel/visitors/visitor.rb', line 86

def visit_with_hash_context_shift(k, v, parent)
  @hash_context_depth += 1

  parent = case k
    when Nodes::KeyPath
      traverse(k, parent, true)
    else
      find(k, parent)
    end

  can_visit?(v) ? visit(v, parent || k) : v
ensure
  @hash_context_depth -= 1
end

#visit_without_hash_context_shift(k, v, parent) ⇒ Object (private)

If there is no context change, the default behavior is to return the value unchanged. Subclasses will alter this behavior as needed.

Parameters:

  • k

    The hash key

  • v

    The hash value

  • parent

    The current parent object in the context

Returns:

  • The same value we just received.



108
109
110
# File 'lib/squeel/visitors/visitor.rb', line 108

def visit_without_hash_context_shift(k, v, parent)
  v
end