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.



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



42
43
44
45
46
47
48
49
# File 'lib/squeel/visitors/visitor.rb', line 42

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.



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

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

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



30
31
32
# File 'lib/squeel/visitors/visitor.rb', line 30

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

#can_visit?(object) ⇒ Boolean



36
37
38
# File 'lib/squeel/visitors/visitor.rb', line 36

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.



73
74
75
# File 'lib/squeel/visitors/visitor.rb', line 73

def hash_context_shifted?
  @hash_context_depth > 0
end

#implies_hash_context_shift?(v) ⇒ Boolean (private)



79
80
81
# File 'lib/squeel/visitors/visitor.rb', line 79

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.



158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
# File 'lib/squeel/visitors/visitor.rb', line 158

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
      if defined?(Arel::Collectors::SQLString)
        Arel.sql(arel_visitor.compile(Arel::Nodes.build_quoted(value)))
      else
        Arel.sql(arel_visitor.accept value)
      end
    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.



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

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

#symbolify(o) ⇒ Object (private)



53
54
55
56
57
58
59
60
# File 'lib/squeel/visitors/visitor.rb', line 53

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.



181
182
183
184
185
186
187
188
189
190
191
192
# File 'lib/squeel/visitors/visitor.rb', line 181

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!(object, parent) ⇒ Object (private)



194
195
196
197
198
# File 'lib/squeel/visitors/visitor.rb', line 194

def visit!(object, parent)
  send("#{DISPATCH[object.class]}!", object, parent)
rescue NoMethodError => e
  visit(object, parent)
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.



452
453
454
# File 'lib/squeel/visitors/visitor.rb', line 452

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



442
443
444
# File 'lib/squeel/visitors/visitor.rb', line 442

def visit_ActiveRecord_Relation(o, parent)
  o.arel
end

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



456
457
458
# File 'lib/squeel/visitors/visitor.rb', line 456

def visit_Arel_Nodes_Node(o, parent)
  o
end

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

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



220
221
222
# File 'lib/squeel/visitors/visitor.rb', line 220

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

#visit_Array!(o, parent) ⇒ Object (private)



224
225
226
# File 'lib/squeel/visitors/visitor.rb', line 224

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.



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

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_Hash!(o, parent) ⇒ Object (private)



244
245
246
247
248
249
250
251
252
# File 'lib/squeel/visitors/visitor.rb', line 244

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.



207
208
209
# File 'lib/squeel/visitors/visitor.rb', line 207

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.



340
341
342
# File 'lib/squeel/visitors/visitor.rb', line 340

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.



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

def visit_Squeel_Nodes_As(o, parent)
  # patch for 4+, binds params using native to_sql before transforms to sql string
  if ::ActiveRecord::VERSION::MAJOR >= 4 && o.left.is_a?(::ActiveRecord::Relation)
    Arel::Nodes::TableAlias.new(
      Arel::Nodes::Grouping.new(
        Arel::Nodes::SqlLiteral.new(
          o.left.respond_to?(:to_sql_with_binding_params) ? o.left.to_sql_with_binding_params : o.left.to_sql
        )
      ),
      o.right
    )
  else
    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
end

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

Visit a Squeel function, returning an Arel NamedFunction node.



377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
# File 'lib/squeel/visitors/visitor.rb', line 377

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
      visit(arg, parent)
    when ActiveRecord::Relation
      arg.arel.ast
    when Symbol, Nodes::Stub
      if defined?(Arel::Collectors::SQLString)
        Arel.sql(arel_visitor.compile(contextualize(parent)[arg.to_s]))
      else
        Arel.sql(arel_visitor.accept(contextualize(parent)[arg.to_s]))
      end
    else
      quote arg
    end
  end

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

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

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



367
368
369
# File 'lib/squeel/visitors/visitor.rb', line 367

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.



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

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

  visit(o.endpoint, parent)
end

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



289
290
291
292
293
# File 'lib/squeel/visitors/visitor.rb', line 289

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



300
301
302
# File 'lib/squeel/visitors/visitor.rb', line 300

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.



358
359
360
# File 'lib/squeel/visitors/visitor.rb', line 358

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)



405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
# File 'lib/squeel/visitors/visitor.rb', line 405

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
      if defined?(Arel::Collectors::SQLString)
        Arel.sql(arel_visitor.compile(contextualize(parent)[arg.to_s]))
      else
        Arel.sql(arel_visitor.accept(contextualize(parent)[arg.to_s]))
      end
    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.



349
350
351
# File 'lib/squeel/visitors/visitor.rb', line 349

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.



272
273
274
# File 'lib/squeel/visitors/visitor.rb', line 272

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.



261
262
263
# File 'lib/squeel/visitors/visitor.rb', line 261

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.



90
91
92
93
94
95
96
97
98
99
100
101
102
103
# File 'lib/squeel/visitors/visitor.rb', line 90

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_with_hash_context_shift!(k, v, parent) ⇒ Object (private)



105
106
107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/squeel/visitors/visitor.rb', line 105

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.



127
128
129
# File 'lib/squeel/visitors/visitor.rb', line 127

def visit_without_hash_context_shift(k, v, parent)
  v
end