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.



60
61
62
# File 'lib/squeel/visitors/visitor.rb', line 60

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



66
67
68
# File 'lib/squeel/visitors/visitor.rb', line 66

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.



127
128
129
130
131
132
133
134
135
136
137
138
139
140
# File 'lib/squeel/visitors/visitor.rb', line 127

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.



111
112
113
114
115
116
117
118
# File 'lib/squeel/visitors/visitor.rb', line 111

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

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

Visit the object.

Parameters:

  • object

    The object to visit

  • parent

    The object’s parent within the context



146
147
148
149
150
151
152
153
154
155
156
157
# File 'lib/squeel/visitors/visitor.rb', line 146

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



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

def visit_ActiveRecord_Base(o, parent)
  o.id
end

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

Visit an ActiveRecord 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



360
361
362
# File 'lib/squeel/visitors/visitor.rb', line 360

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



178
179
180
# File 'lib/squeel/visitors/visitor.rb', line 178

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.



188
189
190
191
192
193
194
195
196
# File 'lib/squeel/visitors/visitor.rb', line 188

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



166
167
168
# File 'lib/squeel/visitors/visitor.rb', line 166

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.



266
267
268
# File 'lib/squeel/visitors/visitor.rb', line 266

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.



247
248
249
250
251
252
253
254
255
256
# File 'lib/squeel/visitors/visitor.rb', line 247

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.



303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
# File 'lib/squeel/visitors/visitor.rb', line 303

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



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

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.



227
228
229
230
231
# File 'lib/squeel/visitors/visitor.rb', line 227

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



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

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



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

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.



327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
# File 'lib/squeel/visitors/visitor.rb', line 327

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



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

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



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

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



205
206
207
# File 'lib/squeel/visitors/visitor.rb', line 205

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



77
78
79
80
81
82
83
84
85
86
87
88
89
90
# File 'lib/squeel/visitors/visitor.rb', line 77

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.



99
100
101
# File 'lib/squeel/visitors/visitor.rb', line 99

def visit_without_hash_context_shift(k, v, parent)
  v
end