lib/roda/plugins/path_rewriter.rb
# frozen-string-literal: true # class Roda module RodaPlugins # The path_rewriter plugin allows you to rewrite the remaining path # or the path info for requests. This is useful if you want to # transparently treat some paths the same as other paths. # # By default, +rewrite_path+ will rewrite just the remaining path. So # only routing in the current Roda app will be affected. This is useful # if you have other code in your app that uses PATH_INFO and needs to # see the original PATH_INFO (for example, when using relative links). # # rewrite_path '/a', '/b' # # PATH_INFO '/a' => remaining_path '/b' # # PATH_INFO '/a/c' => remaining_path '/b/c' # # In some cases, you may want to override PATH_INFO for the rewritten # paths, such as when you are passing the request to another Rack app. # For those cases, you can use the <tt>path_info: true</tt> option to # +rewrite_path+. # # rewrite_path '/a', '/b', path_info: true # # PATH_INFO '/a' => PATH_INFO '/b' # # PATH_INFO '/a/c' => PATH_INFO '/b/c' # # If you pass a string to +rewrite_path+, it will rewrite all paths starting # with that string. You can provide a regexp if you want more complete control, # such as only matching exact paths. # # rewrite_path /\A\/a\z/, '/b' # # PATH_INFO '/a' => remaining_path '/b' # # PATH_INFO '/a/c' => remaining_path '/a/c', no change # # Patterns can be rewritten dynamically by providing a block accepting a MatchData # object and evaluating to the replacement. # # rewrite_path(/\A\/a\/(\w+)/){|match| "/a/#{match[1].capitalize}"} # # PATH_INFO '/a/moo' => remaining_path '/a/Moo' # rewrite_path(/\A\/a\/(\w+)/, path_info: true){|match| "/a/#{match[1].capitalize}"} # # PATH_INFO '/a/moo' => PATH_INFO '/a/Moo' # # All path rewrites are applied in order, so if a path is rewritten by one rewrite, # it can be rewritten again by a later rewrite. Note that PATH_INFO rewrites are # processed before remaining_path rewrites. module PathRewriter def self.configure(app) app.instance_exec do app.opts[:remaining_path_rewrites] ||= [] app.opts[:path_info_rewrites] ||= [] end end module ClassMethods # Freeze the path rewrite metadata. def freeze opts[:remaining_path_rewrites].freeze opts[:path_info_rewrites].freeze super end # Record a path rewrite from path +was+ to path +is+. Options: # :path_info :: Modify PATH_INFO, not just remaining path. def rewrite_path(was, is = nil, opts=OPTS, &block) if is.is_a? Hash raise RodaError, "cannot provide two hashes to rewrite_path" unless opts.empty? opts = is is = nil end if block raise RodaError, "cannot provide both block and string replacement to rewrite_path" if is is = block end was = /\A#{Regexp.escape(was)}/ unless was.is_a?(Regexp) array = @opts[opts[:path_info] ? :path_info_rewrites : :remaining_path_rewrites] array << [was, is.dup.freeze].freeze end end module RequestMethods # Rewrite remaining_path and/or PATH_INFO based on the path rewrites. def initialize(scope, env) path_info = env['PATH_INFO'] rewrite_path(scope.class.opts[:path_info_rewrites], path_info) super remaining_path = @remaining_path = @remaining_path.dup rewrite_path(scope.class.opts[:remaining_path_rewrites], remaining_path) end private # Rewrite the given path using the given replacements. def rewrite_path(replacements, path) replacements.each do |was, is| if is.is_a?(Proc) path.sub!(was){is.call($~)} else path.sub!(was, is) end end end end end register_plugin(:path_rewriter, PathRewriter) end end