lib/roda/plugins/multi_route.rb



# frozen-string-literal: true

#
class Roda
  module RodaPlugins
    # The multi_route plugin builds on the named_routes plugin and allows for
    # dispatching to multiple named routes # by calling the +r.multi_route+ method,
    # which will check # if the first segment in the path matches a named route,
    # and dispatch to that named route.
    #
    # The hash_branches plugin offers a +r.hash_branches+ method that is similar to
    # and performs better than the +r.multi_route+ method, and it is recommended
    # to consider using that instead of this plugin.
    #
    # Example:
    #
    #   plugin :multi_route
    #
    #   route('foo') do |r|
    #     r.is 'bar' do
    #       '/foo/bar'
    #     end
    #   end
    #
    #   route('bar') do |r|
    #     r.is 'foo' do
    #       '/bar/foo'
    #     end
    #   end
    #
    #   route do |r|
    #     r.multi_route
    #   end
    #
    # Note that only named routes with string names will be dispatched to by the
    # +r.multi_route+ method. Named routes with other names can be dispatched to
    # using the named_routes plugin API, but will not be automatically dispatched
    # to by +r.multi_route+.
    #
    # You can provide a block to +r.multi_route+ that is
    # called if the route matches but the named route did not handle the
    # request:
    #
    #   r.multi_route do
    #     "default body"
    #   end
    # 
    # If a block is not provided to multi_route, the return value of the named
    # route block will be used.
    #
    # == Namespace Support
    #
    # The multi_route plugin also has support for namespaces, allowing you to
    # use +r.multi_route+ at multiple levels in your routing tree.  Example:
    #
    #   route('foo') do |r|
    #     r.multi_route('foo')
    #   end
    #
    #   route('bar') do |r|
    #     r.multi_route('bar')
    #   end
    #
    #   route('baz', 'foo') do |r|
    #     # handles /foo/baz prefix
    #   end
    #
    #   route('quux', 'foo') do |r|
    #     # handles /foo/quux prefix
    #   end
    #
    #   route('baz', 'bar') do |r|
    #     # handles /bar/baz prefix
    #   end
    #
    #   route('quux', 'bar') do |r|
    #     # handles /bar/quux prefix
    #   end
    #
    #   route do |r|
    #     r.multi_route
    #   end
    module MultiRoute
      def self.load_dependencies(app)
        app.plugin :named_routes
      end

      # Initialize storage for the named routes.
      def self.configure(app)
        app::RodaRequest.instance_variable_set(:@namespaced_route_regexps, {})
      end

      module ClassMethods
        # Freeze the multi_route regexp matchers so that there can be no thread safety issues at runtime.
        def freeze
          super
          opts[:namespaced_routes].each_key do |k|
            self::RodaRequest.named_route_regexp(k)
          end
          self::RodaRequest.instance_variable_get(:@namespaced_route_regexps).freeze
          self
        end

        # Copy the named routes into the subclass when inheriting.
        def inherited(subclass)
          super
          subclass::RodaRequest.instance_variable_set(:@namespaced_route_regexps, {})
        end

        # Clear the multi_route regexp matcher for the namespace.
        def route(name=nil, namespace=nil, &block)
          super
          if name
            self::RodaRequest.clear_named_route_regexp!(namespace)
          end
        end
      end

      module RequestClassMethods
        # Clear cached regexp for named routes, it will be regenerated
        # the next time it is needed.
        #
        # This shouldn't be an issue in production applications, but
        # during development it's useful to support new named routes
        # being added while the application is running.
        def clear_named_route_regexp!(namespace=nil)
          @namespaced_route_regexps.delete(namespace)
        end

        # A regexp matching any of the current named routes.
        def named_route_regexp(namespace=nil)
          @namespaced_route_regexps[namespace] ||= /(#{Regexp.union(roda_class.named_routes(namespace).select{|s| s.is_a?(String)}.sort.reverse)})/
        end
      end

      module RequestMethods
        # Check if the first segment in the path matches any of the current
        # named routes.  If so, call that named route.  If not, do nothing.
        # If the named route does not handle the request, and a block
        # is given, yield to the block.
        def multi_route(namespace=nil)
          on self.class.named_route_regexp(namespace) do |section|
            res = route(section, namespace)
            if defined?(yield)
              yield
            else
              res
            end
          end
        end
      end
    end

    register_plugin(:multi_route, MultiRoute)
  end
end