lib/roda/plugins/static_routing.rb



# frozen-string-literal: true

#
class Roda
  module RodaPlugins
    # The static_routing plugin adds static_* routing class methods for handling
    # static routes (i.e. routes with static paths, no nesting or placeholders).  These
    # routes are processed before the normal routing tree and designed for
    # maximum performance.  This can be substantially faster than Roda's normal
    # tree based routing if you have large numbers of static routes, about 3-4x
    # for 100-10000 static routes.  Example:
    #
    #   plugin :static_routing
    #   
    #   static_route '/foo' do |r|
    #     @var = :foo
    #
    #     r.get do
    #       'Not actually reached'
    #     end
    #
    #     r.post{'static POST /#{@var}'}
    #   end
    #
    #   static_get '/foo' do |r|
    #     'static GET /foo'
    #   end
    #
    #   route do |r|
    #     'Not a static route'
    #   end
    #
    # A few things to note in the above example.  First, unlike most other
    # routing methods in Roda, these take the full path of the request, and only
    # match if r.path_info matches exactly.  This is why you need to include the
    # leading slash in the path argument.
    #
    # Second, the static_* routing methods only take a single string argument for
    # the path, they do not acccept other options, and do not handle placeholders
    # in strings.  For any routes needing placeholders, you should use Roda's
    # routing tree.
    #
    # There are separate static_* methods for each type of request method, and these
    # request method specific routes are tried first.  There is also a static_route
    # method that will match regardless of the request method, if there is no
    # matching request methods specific route.  This is why the static_get
    # method call takes precedence over the static_route method call for /foo.
    # As shown above, you can use Roda's routing tree methods inside the
    # static_route block to have shared behavior for different request methods,
    # while still handling the request methods differently.
    #
    # Note that if you want to use the static_routing plugin and the hooks
    # plugin at the same time, you should load the hooks plugin first.
    module StaticRouting
      def self.configure(app)
        app.opts[:static_routes] = {}
      end

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

        # Duplicate static route metadata in subclass.
        def inherited(subclass)
          super
          static_routes = subclass.opts[:static_routes]
          opts[:static_routes].each do |k, v|
            static_routes[k] = v.dup
          end
        end

        # Add a static route for any request method.  These are
        # tried after the request method specific static routes (e.g.
        # static_get), but allow you to use Roda's routing tree
        # methods inside the route for handling shared behavior while
        # still allowing request method specific handling.
        def static_route(path, &block)
          add_static_route(nil, path, &block)
        end
        
        # Return the static route for the given request method and path.
        def static_route_for(method, path)
          if h = opts[:static_routes][path]
            h[method] || h[nil]
          end
        end

        [:get, :post, :delete, :head, :options, :link, :patch, :put, :trace, :unlink].each do |meth|
          request_method = meth.to_s.upcase
          define_method("static_#{meth}") do |path, &block|
            add_static_route(request_method, path, &block)
          end
        end

        private

        # Add a static route for the given method.
        def add_static_route(method, path, &block)
          (opts[:static_routes][path] ||= {})[method] = block
        end
      end

      module InstanceMethods
        # If there is a static routing method for the given path, call it
        # instead having the routing tree handle the request.
        def call(&block)
          super do |r|
            if route = self.class.static_route_for(r.request_method, r.path_info)
              r.static_route(&route)
            else
              instance_exec(r, &block)
            end
          end
        end
      end

      module RequestMethods
        # Assume that this request matches a static route, setting
        # the remaining path to the emptry string and passing
        # control to the given block.
        def static_route(&block)
          @remaining_path = ''

          always do
            scope.instance_exec(self, &block)
          end
        end
      end
    end

    register_plugin(:static_routing, StaticRouting)
  end
end