module Merb class Router SECTION_REGEXP = /(?::([a-z*_]+))/.freeze class << self def prepare @@matcher = RouteMatcher.new @@generator = RouteGenerator.new yield self @@matcher.compile_router end def add(*route) @@matcher.add(*route) end def matcher @@matcher end def generator @@generator end def match(path) @@matcher.route_request(path) end def generate(method, *args) @@generator.generate(method, *args) end def resources(res, opts={}) opts[:prefix] ||= "" if block_given? procs = [] yield Resource.new(res, procs, opts) procs.reverse.each &:call else generate_resources_routes(res,opts) end end def resource(res, opts={}) opts[:prefix] ||= "" if block_given? procs = [] yield Resource.new(res, procs, opts) procs.reverse.each &:call else generate_singleton_routes(res,opts) end end def default_routes(*a) @@matcher.default_routes(*a) end def compiled_statement @@matcher.compiled_statement end def compiled_regexen @@matcher.compiled_regexen end def generate_resources_routes(res, opts) [@@matcher,@@generator].each { |r| r.generate_resources_routes(res, opts) } end def generate_singleton_routes(res, opts) [@@matcher,@@generator].each { |r| r.generate_singleton_routes(res, opts) } end end # class << self class RouteMatcher attr_reader :routes, :compiled_statement, :compiled_regexen def initialize @routes = Array.new # The final compiled lambda that gets used as the body of the route_request method. @compiled_statement = String.new @compiled_regexen = Array.new end # Add a route to be compiled. def add(*route) opt = Hash === route.last ? route.pop : {} if n = opt[:namespace] path = "/#{n}#{route[0]}" else path = route[0] end @routes << [path, opt] end def raw_add(*route) @routes << [route[0], (route[1]||{})] end # Build up a string that defines a lambda that does a case statement on # the PATH_INFO against each of the compiled routes in turn. First route # that matches wins. def compile_router router_lambda = @routes.inject("lambda{|path| \n sections={}\n case path\n") { |m,r| m << compile(r) } << " else\n return {:controller=>'Noroutefound', :action=>'noroute'}\n end\n}" @compiled_statement = router_lambda meta_def(:route_request, &eval(router_lambda)) end # Compile each individual route into a when /.../ component of the case # statement. Takes /:sections of the route def that start with : and # turns them into placeholders for whatever urls match against the route # in question. def compile(route) raise ArgumentError unless String === route[0] code, count = '', 0 while route[0] =~ Router::SECTION_REGEXP route[0] = route[0].dup name = $1 (name =~ /(\*+)(\w+)/) ? (flag = true; name = $2) : (flag = false) count += 1 if flag route[0].sub!(Router::SECTION_REGEXP, "([^,?]+)") else route[0].sub!(Router::SECTION_REGEXP, "([^\/,?]+)") end code << " sections[:#{name}] = $#{count}\n" end @compiled_regexen << Regexp.new(route[0]) index = @compiled_regexen.size - 1 condition = " when @compiled_regexen[#{index}] " statement = "#{condition}\n#{code}" statement << " return #{route[1].inspect}.update(sections)\n" statement end def generate_resources_routes(res,opt) with_options opt.merge(:controller => res.to_s, :rest => true) do |r| r.raw_add "#{opt[:prefix]}/#{res}/:id[;/]edit", :allowed => {:get => 'edit'} r.raw_add "#{opt[:prefix]}/#{res}/new[;/]:action", :allowed => {:get => 'new', :post => 'new', :put => 'new', :delete => 'new'} r.raw_add "#{opt[:prefix]}/#{res}/new" , :allowed => {:get => 'new'} if mem = opt[:member] mem.keys.sort_by{|x| "#{x}"}.each {|action| allowed = mem[action].injecting({}) {|h, verb| h[verb] = "#{action}"} r.raw_add "#{opt[:prefix]}/#{res}/:id[;/]+#{action}", :allowed => allowed } end if coll = opt[:collection] coll.keys.sort_by{|x| "#{x}"}.each {|action| allowed = coll[action].injecting({}) {|h, verb| h[verb] = "#{action}"} r.raw_add "#{opt[:prefix]}/#{res}[;/]#{action}", :allowed => allowed } end r.raw_add "#{opt[:prefix]}/#{res}/:id\\.:format", :allowed => {:get => 'show', :put => 'update', :delete => 'destroy'} r.raw_add "#{opt[:prefix]}/#{res}\\.:format", :allowed => {:get => 'index', :post => 'create'} r.raw_add "#{opt[:prefix]}/#{res}/:id", :allowed => {:get => 'show', :put => 'update', :delete => 'destroy'} r.raw_add "#{opt[:prefix]}/#{res}/?", :allowed => {:get => 'index', :post => 'create'} end end def generate_singleton_routes(res,opt) with_options opt.merge(:controller => res.to_s, :rest => true ) do |r| r.raw_add "#{opt[:prefix]}/#{res}[;/]edit", :allowed => {:get => 'edit'} r.raw_add "#{opt[:prefix]}/#{res}\\.:format", :allowed => {:get => 'show'} r.raw_add "#{opt[:prefix]}/#{res}/new" , :allowed => {:get => 'new'} r.raw_add "#{opt[:prefix]}/#{res}/?", :allowed => {:get => 'show', :post => 'create', :put => 'update', :delete => 'destroy'} end end def default_routes(opt={}) namespace = opt[:namespace] ? "/#{opt[:namespace]}" : "" with_options opt do |r| r.raw_add namespace + "/:controller/:action/:id\\.:format" r.raw_add namespace + "/:controller/:action/:id" r.raw_add namespace + "/:controller/:action\\.:format" r.raw_add namespace + "/:controller/:action" r.raw_add namespace + "/:controller\\.:format", :action => 'index' r.raw_add namespace + "/:controller", :action => 'index' end end end class RouteGenerator attr_accessor :paths def initialize @paths = {} end def add(name, path) @paths[name.to_sym] = path end def generate(name, *args) options = Hash === args.last ? args.pop : {} obj = args[0] options.each do |key, value| next unless value.respond_to?(:to_param) unless key.to_s =~ /_?id$/ old_key = key options[old_key] = value.to_param key = "#{key}_id".intern end options[key] = value.to_param end path = @paths[name].dup while path =~ Router::SECTION_REGEXP if obj.respond_to?($1) && ! obj.nil? path.sub!(Router::SECTION_REGEXP, obj.send($1).to_s) else path.sub!(Router::SECTION_REGEXP, options[$1.intern].to_s) end end if f = options[:format] "#{path}.#{f}" else path end end def generate_singleton_routes(res,opt) res = res.to_s if opt[:namespace] namespace = "#{opt[:namespace]}_" name = "/#{opt[:namespace]}" else namespace = '' name = '' end add namespace + "edit_#{res}", name + "#{opt[:prefix]}/#{res}/edit" add namespace + "new_#{res}", name + "#{opt[:prefix]}/#{res}/new" add namespace + res, name + "#{opt[:prefix]}/#{res}" end def generate_resources_routes(res,opt) res = res.to_s res_singular = res.singularize if opt[:namespace] namespace = "#{opt[:namespace]}_" name = "/#{opt[:namespace]}" else namespace = '' name = '' end add namespace + res, name + "#{opt[:prefix]}/#{res}" add namespace + res_singular, name + "#{opt[:prefix]}/#{res}/:id" add namespace + "new_#{res_singular}", name + "#{opt[:prefix]}/#{res}/new" add namespace + "custom_new_#{res_singular}", name + "#{opt[:prefix]}/#{res}/new/:action" add namespace + "edit_#{res_singular}", name + "#{opt[:prefix]}/#{res}/:id/edit" if mem = opt[:member] mem.keys.sort_by{|x| "#{x}"}.each {|action| add namespace + "#{action}_#{res_singular}", name + "#{opt[:prefix]}/#{res}/:id/#{action}" } end if coll = opt[:collection] coll.keys.sort_by{|x| "#{x}"}.each {|action| add namespace + "#{action}_#{res_singular}", name + "#{opt[:prefix]}/#{res}/#{action}" } end end end class Resource def initialize(resource, procs=[], opts={}) @resource, @procs, @opts = resource, procs, opts @procs << proc do [::Merb::Router.matcher, ::Merb::Router.generator].each {|r| r.generate_resources_routes(@resource, @opts) } end end def resources(res, opts={}) (opts[:prefix]||='') << "/#{@resource}/:#{@resource.to_s.singularize}_id" opts[:prefix] = @opts[:prefix] + opts[:prefix] if block_given? yield self.class.new(res, @procs, opts) else @procs << proc do [::Merb::Router.matcher, ::Merb::Router.generator].each {|r| r.generate_resources_routes(res, opts) } end end end def resource(res, opts={}) (opts[:prefix]||='') << "/#{@resource}/:#{@resource.to_s.singularize}_id" opts[:prefix] = @opts[:prefix] + opts[:prefix] if block_given? yield self.class.new(res, @procs, opts) else @procs << proc do [::Merb::Router.matcher, ::Merb::Router.generator].each { |r| r.generate_singleton_routes(res, opts) } end end end end end end