mvgijssel/arel_toolkit

View on GitHub
lib/arel/enhance/node.rb

Summary

Maintainability
A
0 mins
Test Coverage
A
97%
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