lib/roda/plugins/named_routes.rb



# frozen-string-literal: true

#
class Roda
  module RodaPlugins
    # The named_routes plugin allows for multiple named routes, which the
    # main route block can dispatch to by name at any point by calling +r.route+.
    # If the named route doesn't handle the request, execution will continue,
    # and if the named route does handle the request, the response returned by
    # the named route will be returned.
    #
    # 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.on "foo" do
    #       r.route 'foo'
    #     end
    #
    #     r.on "bar" do
    #       r.route 'bar'
    #     end
    #   end
    #
    # Note that in multi-threaded code, you should not attempt to add a
    # named route after accepting requests.
    #
    # To handle development environments that reload code, you can call the
    # +route+ class method without a block to remove an existing named route.
    #
    # == Routing Files
    #
    # The convention when using the named_routes plugin is to have a single
    # named route per file, and these routing files should be stored in
    # a routes subdirectory in your application.  So for the above example, you
    # would use the following files:
    #
    #   routes/bar.rb
    #   routes/foo.rb
    #
    # == Namespace Support
    #
    # The named_routes plugin also has support for namespaces, allowing you to
    # use +r.route+ at multiple levels in your routing tree.  Example:
    #
    #   route('foo') do |r|
    #     r.on("baz"){r.route("baz", "foo")}
    #     r.on("quux"){r.route("quux", "foo")}
    #   end
    #
    #   route('bar') do |r|
    #     r.on("baz"){r.route("baz", "bar")}
    #     r.on("quux"){r.route("quux", "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.on "foo" do
    #       r.route("foo")
    #     end
    #
    #     r.on "bar" do
    #       r.route("bar")
    #     end
    #   end
    #
    # === Routing Files
    #
    # The convention when using namespaces with the multi_route plugin is to
    # store the routing files in subdirectories per namespace. So for the
    # above example, you would have the following routing files:
    #
    #   routes/bar.rb
    #   routes/bar/baz.rb
    #   routes/bar/quux.rb
    #   routes/foo.rb
    #   routes/foo/baz.rb
    #   routes/foo/quux.rb
    module NamedRoutes
      # Initialize storage for the named routes.
      def self.configure(app)
        app.opts[:namespaced_routes] ||= {}
      end

      module ClassMethods
        # Freeze the namespaced routes so that there can be no thread safety issues at runtime.
        def freeze
          opts[:namespaced_routes].freeze.each_value(&:freeze)
          super
        end

        # Copy the named routes into the subclass when inheriting.
        def inherited(subclass)
          super
          nsr = subclass.opts[:namespaced_routes]
          opts[:namespaced_routes].each{|k, v| nsr[k] = v.dup}
        end

        # The names for the currently stored named routes
        def named_routes(namespace=nil)
          unless routes = opts[:namespaced_routes][namespace]
            raise RodaError, "unsupported named_routes namespace used: #{namespace.inspect}"
          end
          routes.keys
        end

        # Return the named route with the given name.
        def named_route(name, namespace=nil)
          opts[:namespaced_routes][namespace][name]
        end

        # If the given route has a name, treat it as a named route and
        # store the route block.  Otherwise, this is the main route, so
        # call super.
        def route(name=nil, namespace=nil, &block)
          if name
            routes = opts[:namespaced_routes][namespace] ||= {}
            if block
              routes[name] = define_roda_method(routes[name] || "named_routes_#{namespace}_#{name}", 1, &convert_route_block(block))
            elsif meth = routes.delete(name)
              remove_method(meth)
            end
          else
            super(&block)
          end
        end
      end

      module RequestMethods
        # Dispatch to the named route with the given name.
        def route(name, namespace=nil)
          scope.send(roda_class.named_route(name, namespace), self)
        end
      end
    end

    register_plugin(:named_routes, NamedRoutes)
  end
end