lib/rage/router/node.rb



# frozen_string_literal: true

module Rage::Router
  class Node
    STATIC = 0
    PARAMETRIC = 1
    WILDCARD = 2

    attr_reader :is_leaf_node, :handler_storage, :kind

    def initialize
      @is_leaf_node = false
      @routes = nil
      @handler_storage = nil
    end

    def add_route(route, constrainer)
      @routes ||= []
      @handler_storage ||= HandlerStorage.new

      @is_leaf_node = true
      @routes << route
      @handler_storage.add_handler(constrainer, route)
    end
  end

  class ParentNode < Node
    attr_reader :static_children

    def initialize
      super
      @static_children = {}
    end

    def find_static_matching_child(path, path_index)
      static_child = @static_children[path[path_index]]

      if !static_child || !static_child.match_prefix.call(path, path_index)
        return nil
      end

      static_child
    end

    def create_static_child(path)
      if path.length == 0
        return self
      end

      static_child = @static_children[path[0]]
      if static_child
        i = 1
        while i < static_child.prefix.length
          if path[i] != static_child.prefix[i]
            static_child = static_child.split(self, i)
            break
          end
          i += 1
        end

        return static_child.create_static_child(path[i, path.length - i])
      end

      @static_children[path[0]] = StaticNode.new(path)
    end
  end

  class StaticNode < ParentNode
    attr_reader :prefix, :match_prefix

    def initialize(prefix)
      super()
      @prefix = prefix
      @wildcard_child = nil
      @parametric_children = []
      @kind = Node::STATIC

      compile_prefix_match
    end

    def create_parametric_child(static_suffix, node_path)
      parametric_child = @parametric_children[0]

      if parametric_child
        parametric_child.node_paths.add(node_path)
        return parametric_child
      end

      parametric_child = ParametricNode.new(static_suffix, node_path)
      @parametric_children << parametric_child
      @parametric_children.sort! do |child1, child2|
        if child1.static_suffix.nil?
          1
        elsif child2.static_suffix.nil?
          -1
        elsif child2.static_suffix.end_with?(child1.static_suffix)
          1
        elsif child1.static_suffix.end_with?(child2.static_suffix)
          -1
        else
          0
        end
      end

      parametric_child
    end

    def create_wildcard_child
      @wildcard_child ||= WildcardNode.new
    end

    def split(parent_node, length)
      parent_prefix = @prefix[0, length]
      child_prefix = @prefix[length, @prefix.length - length]

      @prefix = child_prefix
      compile_prefix_match

      static_node = StaticNode.new(parent_prefix)
      static_node.static_children[child_prefix[0]] = self
      parent_node.static_children[parent_prefix[0]] = static_node

      static_node
    end

    def get_next_node(path, path_index, node_stack, params_count)
      node = find_static_matching_child(path, path_index)
      parametric_brother_node_index = 0

      unless node
        return @wildcard_child if @parametric_children.empty?

        node = @parametric_children[0]
        parametric_brother_node_index = 1
      end

      if @wildcard_child
        node_stack << {
          params_count: params_count,
          brother_path_index: path_index,
          brother_node: @wildcard_child
        }
      end

      i = @parametric_children.length - 1
      while i >= parametric_brother_node_index
        node_stack << {
          params_count: params_count,
          brother_path_index: path_index,
          brother_node: @parametric_children[i]
        }
        i -= 1
      end

      node
    end

    private

    def compile_prefix_match
      if @prefix.length == 1
        @match_prefix = ->(_, _) { true }
        return
      end

      lines = (1...@prefix.length).map do |i|
        "path[i + #{i}]&.ord == #{@prefix[i].ord}"
      end

      @match_prefix = eval("->(path, i) { #{lines.join(" && ")} }")
    end
  end

  class ParametricNode < ParentNode
    attr_reader :static_suffix, :node_paths

    def initialize(static_suffix, node_path)
      super()
      @static_suffix = static_suffix
      @kind = Node::PARAMETRIC

      @node_paths = Set.new([node_path])
    end

    def get_next_node(path, path_index, _, _)
      find_static_matching_child(path, path_index)
    end
  end

  class WildcardNode < Node
    def initialize
      super
      @kind = Node::WILDCARD
    end

    def get_next_node(*)
      nil
    end
  end
end