lib/roda/plugins/hash_paths.rb
# frozen-string-literal: true # class Roda module RodaPlugins # The hash_paths plugin allows for O(1) dispatch to multiple routes at any point # in the routing tree. It is useful when you have a large number of specific routes # to dispatch to at any point in the routing tree. # # You configure the hash paths to dispatch to using the +hash_path+ class method, # specifying the remaining path, with a block to handle that path. Then you dispatch # to the configured paths using +r.hash_paths+: # # class App < Roda # plugin :hash_paths # # hash_path("/a") do |r| # # /a path # end # # hash_path("/a/b") do |r| # # /a/b path # end # # route do |r| # r.hash_paths # end # end # # With the above routing tree, the +r.hash_paths+ call will dispatch requests for the +/a+ and # +/a/b+ request paths. # # The +hash_path+ class method supports namespaces, which allows +r.hash_paths+ to be used at # any level of the routing tree. Here is an example that uses namespaces for sub-branches: # # class App < Roda # plugin :hash_paths # # # Two arguments provided, so first argument is the namespace # hash_path("/a", "/b") do |r| # # /a/b path # end # # hash_path("/a", "/c") do |r| # # /a/c path # end # # hash_path(:b, "/b") do |r| # # /b/b path # end # # hash_path(:b, "/c") do |r| # # /b/c path # end # # route do |r| # r.on 'a' do # # No argument given, so uses the already matched path as the namespace, # # which is '/a' in this case. # r.hash_paths # end # # r.on 'b' do # # uses :b as the namespace when looking up routes, as that was explicitly specified # r.hash_paths(:b) # end # end # end # # With the above routing tree, requests for the +/a+ branch will be handled by the first # +r.hash_paths+ call, and requests for the +/b+ branch will be handled by the second # +r.hash_paths+ call. Those will dispatch to the configured hash paths for the +/a+ and # +:b+ namespaces. # # It is best for performance to explicitly specify the namespace when calling # +r.hash_paths+. module HashPaths def self.configure(app) app.opts[:hash_paths] ||= {} end module ClassMethods # Freeze the hash_paths metadata when freezing the app. def freeze opts[:hash_paths].freeze.each_value(&:freeze) super end # Duplicate hash_paths metadata in subclass. def inherited(subclass) super h = subclass.opts[:hash_paths] opts[:hash_paths].each do |namespace, routes| h[namespace] = routes.dup end end # Add path handler for the given namespace and path. When the # r.hash_paths method is called, checks the matching namespace # for the full remaining path, and dispatch to that block if # there is one. If called without a block, removes the existing # path handler if it exists. def hash_path(namespace='', path, &block) routes = opts[:hash_paths][namespace] ||= {} if block routes[path] = define_roda_method(routes[path] || "hash_path_#{namespace}_#{path}", 1, &convert_route_block(block)) elsif meth = routes.delete(path) remove_method(meth) end end end module RequestMethods # Checks the matching hash_path namespace for a branch matching the # remaining path, and dispatch to that block if there is one. def hash_paths(namespace=matched_path) if (routes = roda_class.opts[:hash_paths][namespace]) && (meth = routes[@remaining_path]) @remaining_path = '' always{scope.send(meth, self)} end end end end register_plugin(:hash_paths, HashPaths) end end