lib/roda/plugins/hash_branches.rb



# frozen-string-literal: true

#
class Roda
  module RodaPlugins
    # The hash_branches plugin allows for O(1) dispatch to multiple route tree branches,
    # based on the next segment in the remaining path:
    #
    #   class App < Roda
    #     plugin :hash_branches
    #
    #     hash_branch("a") do |r|
    #       # /a branch
    #     end
    #
    #     hash_branch("b") do |r|
    #       # /b branch
    #     end
    #
    #     route do |r|
    #       r.hash_branches
    #     end
    #   end
    #
    # With the above routing tree, the +r.hash_branches+ call in the main routing tree
    # will dispatch requests for the +/a+ and +/b+ branches of the tree to the appropriate
    # routing blocks.
    #
    # In this example, the hash branches for +/a+ and +/b+ are in the same file, but in larger
    # applications, they are usually stored in separate files.  This allows for easily splitting
    # up the routing tree into a separate file per branch.
    #
    # The +hash_branch+ method supports namespaces, which allow for dispatching to sub-branches
    # any level of the routing tree, fully supporting the needs of applications with large and
    # deep routing branches:
    #
    #   class App < Roda
    #     plugin :hash_branches
    #
    #     # Only one argument used, so the namespace defaults to '', and the argument
    #     # specifies the route name
    #     hash_branch("a") do |r|
    #       # No argument given, so uses the already matched path as the namespace,
    #       # which is '/a' in this case.
    #       r.hash_branches
    #     end
    #
    #     hash_branch("b") do |r|
    #       # uses :b as the namespace when looking up routes, as that was explicitly specified
    #       r.hash_branches(:b)
    #     end
    #
    #     # Two arguments used, so first specifies the namespace and the second specifies
    #     # the branch name
    #     hash_branch("/a", "b") do |r|
    #       # /a/b path
    #     end
    #
    #     hash_branch("/a", "c") do |r|
    #       # /a/c path 
    #     end
    #
    #     hash_branch(:b, "b") do |r|
    #       # /b/b path
    #     end
    #
    #     hash_branch(:b, "c") do |r|
    #       # /b/c path 
    #     end
    #
    #     route do |r|
    #       # No argument given, so uses '' as the namespace, as no part of the path has
    #       # been matched yet
    #       r.hash_branches
    #     end
    #   end
    #
    # With the above routing tree, requests for the +/a+ and +/b+ branches will be
    # dispatched to the appropriate +hash_branch+ block.  Those blocks will the dispatch
    # to the remaining +hash_branch+ blocks, with the +/a+ branch using the implicit namespace of
    # +/a+, and the +/b+ branch using the explicit namespace of +:b+.
    #
    # It is best for performance to explicitly specify the namespace when calling
    # +r.hash_branches+.
    module HashBranches
      def self.configure(app)
        app.opts[:hash_branches] ||= {}
      end

      module ClassMethods
        # Freeze the hash_branches metadata when freezing the app.
        def freeze
          opts[:hash_branches].freeze.each_value(&:freeze)
          super
        end

        # Duplicate hash_branches metadata in subclass.
        def inherited(subclass)
          super

          h = subclass.opts[:hash_branches]
          opts[:hash_branches].each do |namespace, routes|
            h[namespace] = routes.dup
          end
        end

        # Add branch handler for the given namespace and segment. If called without
        # a block, removes the existing branch handler if it exists.
        def hash_branch(namespace='', segment, &block)
          segment = "/#{segment}"
          routes = opts[:hash_branches][namespace] ||= {}
          if block
            routes[segment] = define_roda_method(routes[segment] || "hash_branch_#{namespace}_#{segment}", 1, &convert_route_block(block))
          elsif meth = routes.delete(segment)
            remove_method(meth)
          end
        end
      end

      module RequestMethods
        # Checks the matching hash_branch namespace for a branch matching the next
        # segment in the remaining path, and dispatch to that block if there is one.
        def hash_branches(namespace=matched_path)
          rp = @remaining_path

          return unless rp.getbyte(0) == 47 # "/"

          if routes = roda_class.opts[:hash_branches][namespace]
            if segment_end = rp.index('/', 1)
              if meth = routes[rp[0, segment_end]]
                @remaining_path = rp[segment_end, 100000000]
                always{scope.send(meth, self)}
              end
            elsif meth = routes[rp]
              @remaining_path = ''
              always{scope.send(meth, self)}
            end
          end
        end
      end
    end

    register_plugin(:hash_branches, HashBranches)
  end
end