src/derails/action_dispatch/journey/nodes/node.rb
# frozen_string_literal: true

# :markup: markdown

require "action_dispatch/journey/visitors"

module ActionDispatch
  module Journey # :nodoc:
    class Ast # :nodoc:
      attr_reader :names, :path_params, :tree, :wildcard_options, :terminals
      alias :root :tree

      def initialize(tree, formatted)
        @tree = tree
        @path_params = []
        @names = []
        @symbols = []
        @stars = []
        @terminals = []
        @wildcard_options = {}

        visit_tree(formatted)
      end

      def requirements=(requirements)
        # inject any regexp requirements for `star` nodes so they can be determined
        # nullable, which requires knowing if the regex accepts an empty string.
        (symbols + stars).each do |node|
          re = requirements[node.to_sym]
          node.regexp = re if re
        end
      end

      def route=(route)
        terminals.each { |n| n.memo = route }
      end

      def glob?
        stars.any?
      end

      private
        attr_reader :symbols, :stars

        def visit_tree(formatted)
          tree.each do |node|
            if node.symbol?
              path_params << node.to_sym
              names << node.name
              symbols << node
            elsif node.star?
              stars << node

              if formatted != false
                # Add a constraint for wildcard route to make it non-greedy and match the
                # optional format part of the route by default.
                wildcard_options[node.name.to_sym] ||= /.+?/m
              end
            end

            if node.terminal?
              terminals << node
            end
          end
        end
    end

    module Nodes # :nodoc:
      class Node # :nodoc:
        include Enumerable

        attr_accessor :left, :memo

        def initialize(left)
          @left = left
          @memo = nil
          @to_s = nil
        end

        def each(&block)
          Visitors::Each::INSTANCE.accept(self, block)
        end

        def to_s
          @to_s ||= Visitors::String::INSTANCE.accept(self, "".dup).freeze
        end

        def to_dot
          Visitors::Dot::INSTANCE.accept(self)
        end

        def to_sym
          name.to_sym
        end

        def name
          -left.tr("*:", "")
        end

        def type
          raise NotImplementedError
        end

        def symbol?; false; end
        def literal?; false; end
        def terminal?; false; end
        def star?; false; end
        def cat?; false; end
        def group?; false; end
      end

      class Terminal < Node # :nodoc:
        alias :symbol :left
        def terminal?; true; end
      end

      class Literal < Terminal # :nodoc:
        def literal?; true; end
        def type; :LITERAL; end
      end

      class Dummy < Literal # :nodoc:
        def initialize(x = Object.new)
          super
        end

        def literal?; false; end
      end

      class Slash < Terminal # :nodoc:
        def type; :SLASH; end
      end

      class Dot < Terminal # :nodoc:
        def type; :DOT; end
      end

      class Symbol < Terminal # :nodoc:
        attr_accessor :regexp
        alias :symbol :regexp
        attr_reader :name

        DEFAULT_EXP = /[^.\/?]+/
        GREEDY_EXP = /(.+)/
        def initialize(left, regexp = DEFAULT_EXP)
          super(left)
          @regexp = regexp
          @name = -left.tr("*:", "")
        end

        def type; :SYMBOL; end
        def symbol?; true; end
      end

      class Unary < Node # :nodoc:
        def children; [left] end
      end

      class Group < Unary # :nodoc:
        def type; :GROUP; end
        def group?; true; end
      end

      class Star < Unary # :nodoc:
        attr_accessor :regexp

        def initialize(left)
          super(left)

          # By default wildcard routes are non-greedy and must match something.
          @regexp = /.+?/m
        end

        def star?; true; end
        def type; :STAR; end

        def name
          left.name.tr "*:", ""
        end
      end

      class Binary < Node # :nodoc:
        attr_accessor :right

        def initialize(left, right)
          super(left)
          @right = right
        end

        def children; [left, right] end
      end

      class Cat < Binary # :nodoc:
        def cat?; true; end
        def type; :CAT; end
      end

      class Or < Node # :nodoc:
        attr_reader :children

        def initialize(children)
          @children = children
        end

        def type; :OR; end
      end
    end
  end
end
