lib/arel/enhance/node.rb
module Arel
module Enhance
class Node
attr_reader :local_path, :object, :parent, :root_node
def initialize(object)
@object = object
@root_node = self
@dirty = false
end
def inspect
recursive_inspect('')
end
def value
return unless value?
fields.first
end
def each(&block)
return enum_for(:each) unless block_given?
yield self
children.each_value do |child|
child.each(&block)
end
end
def value?
children.empty?
end
def dirty?
root_node.instance_values.fetch('dirty')
end
def remove
mutate(nil, remove: true)
end
def replace(new_arel_node)
mutate(new_arel_node)
end
def add(path_node, node)
node.local_path = path_node
node.parent = self
node.root_node = root_node
children[path_node.value.to_s] = node
end
def to_sql(engine = Table.engine)
return nil if children.empty?
if object.respond_to?(:to_sql)
object.to_sql(engine)
else
collector = Arel::Collectors::SQLString.new
collector = engine.connection.visitor.accept object, collector
collector.value
end
end
def to_sql_and_binds(engine = Table.engine)
object.to_sql_and_binds(engine)
end
def method_missing(name, *args, &block)
child = children[name.to_s]
return super if child.nil?
child
end
def respond_to_missing?(method, include_private = false)
child = children[method.to_s]
child.present? || super
end
def [](key)
children.fetch(key.to_s)
end
def child_at_path(path_items)
selected_node = self
path_items.each do |path_item|
selected_node = selected_node[path_item]
return nil if selected_node.nil?
end
selected_node
end
def query(**kwargs)
Arel::Enhance::Query.call(self, kwargs)
end
def full_path
the_path = [local_path]
current_parent = parent
while current_parent
the_path.unshift current_parent.local_path
current_parent = current_parent.parent
end
the_path.compact
end
def children
@children ||= {}
end
def fields
@fields ||= []
end
def context
@context ||= {}
end
protected
attr_writer :local_path, :parent, :root_node
# rubocop:disable Metrics/AbcSize
# rubocop:disable Metrics/CyclomaticComplexity
# rubocop:disable Metrics/PerceivedComplexity
def recursive_inspect(string, indent = 1)
string << "<#{inspect_name} #{full_path.inspect}\n"
string << "#{spacing(indent)}sql = #{to_sql}\n" unless to_sql.nil?
string << "#{spacing(indent)}parent = #{parent.nil? ? nil.inspect : parent.inspect_name}"
string << "\n" unless children.length.zero?
children.each do |key, child|
string << "#{spacing(indent)}#{key} =\n"
string << spacing(indent + 1)
child.recursive_inspect(string, indent + 2)
end
string << "\n" if children.length.zero? && value?
string << "#{spacing(indent)}value = #{value.inspect}" if value?
string << if children.length.zero?
">\n"
else
"#{spacing(indent - 1)}>\n"
end
end
# rubocop:enable Metrics/AbcSize
# rubocop:enable Metrics/CyclomaticComplexity
# rubocop:enable Metrics/PerceivedComplexity
attr_writer :object
def inspect_name
"Node(#{object.class.name})"
end
def spacing(indent)
indent.times.reduce('') do |memo|
memo << ' '
memo
end
end
def deep_copy_object
# https://github.com/mvgijssel/arel_toolkit/issues/97
new_object = Marshal.load(Marshal.dump(object))
self.object = new_object
recursive_update_object(new_object)
end
def recursive_update_object(arel_tree)
children.each_value do |child|
tree_child = arel_tree.send(*child.local_path.method)
child.object = tree_child
child.recursive_update_object(tree_child)
end
end
def mark_as_dirty
return if dirty?
@dirty = true
deep_copy_object
end
private
# rubocop:disable Metrics/PerceivedComplexity
# rubocop:disable Metrics/CyclomaticComplexity
# rubocop:disable Metrics/AbcSize
def mutate(new_node, remove: false)
root_node.mark_as_dirty
parent_object = parent.object
new_arel_node = new_node.is_a?(Arel::Enhance::Node) ? new_node.object : new_node
new_arel_node = [] if remove && object.is_a?(Array)
if parent_object.respond_to?("#{local_path.value}=")
parent_object.send("#{local_path.value}=", new_arel_node)
elsif parent_object.instance_values.key?(local_path.value)
parent_object.instance_variable_set("@#{local_path.value}", new_arel_node)
elsif local_path.arguments? && parent_object.respond_to?(local_path.method[0])
if remove
parent_object.delete_at(local_path.value)
else
parent_object[local_path.value] = new_arel_node
end
elsif parent_object.is_a?(Arel::Nodes::TableAlias) && local_path.value == 'relation'
parent_object.instance_variable_set('@left', new_arel_node)
else
raise "Don't know how to replace `#{local_path.value}` in #{parent_object.inspect}"
end
if new_node.is_a?(Arel::Enhance::Node)
parent.add(local_path, new_node)
parent[local_path.value]
else
new_parent_tree = Visitor.new.accept_with_root(parent_object, parent)
parent.parent.add(parent.local_path, new_parent_tree)
new_parent_tree[local_path.value]
end
end
# rubocop:enable Metrics/PerceivedComplexity
# rubocop:enable Metrics/CyclomaticComplexity
# rubocop:enable Metrics/AbcSize
end
end
end