Skip to content

Instantly share code, notes, and snippets.

@jm
Created December 31, 2008 23:53
Show Gist options
  • Save jm/42156 to your computer and use it in GitHub Desktop.
Save jm/42156 to your computer and use it in GitHub Desktop.
# Put me in an initializer!
module ActionController
module Routing
class RouteSet
attr_accessor :routes
def initialize
@routes = []
@route_structure = {}
end
def add_named_route(name, path, options = {})
# TODO: Setup named routes hash so we can Merb-style url() calls
add_route(path, options)
end
def add_route(route_string, *args)
# TODO: blah* parameters
@route_structure ||= {}
@routes ||= []
params = []
args = (args.pop || {})
# Set the request method; default to GET
request_method = args[:conditions] && args[:conditions][:method] ? args[:conditions].delete(:method) : :get
# Grab the requirements from the :requirements param or from any arg that's a regex
requirements = args.delete(:requirements) || {}
args.each do |k, v|
requirements[k.to_sym] = args[k] if v.is_a?(Regexp)
end
# Create segment collection for local analysis of a route (i.e., parameter interpolation)
local_segments = []
# Split the route string into segments
segments = route_string.split("/").map! do |segment|
next if segment.blank?
# Escape the segments so we can push them to a regex later on
segment = local_segment = Regexp.escape(segment)
# If there's a dynamic symbol...
if segment =~ /:\w+/
# Grab all the symbols
segment_symbols = segment.scan(/:(\w+)/).flatten
segment_symbols.each do |segment_symbol|
# Make a note of the parameter name
params << segment_symbol.to_sym
# This regex will be used to interpolate the parameters; the captures are finer grained
local_segment = segment.gsub(/:#{segment_symbol}/, "((#{requirements[segment_symbol.to_sym] || '.*'}))")
# This regex will be used to match the route to a path
segment.gsub!(/:#{segment_symbol}/, "#{requirements[segment_symbol.to_sym] || '.*'}")
end
elsif segment =~ /^\*w+/
# Route globbing
params << segment.to_sym
local_segment = segment.gsub(/:#{segment_symbol}/, "((#{requirements[segment_symbol.to_sym] || '.*'}))")
segment.gsub!(/:#{segment_symbol}/, "#{requirements[segment_symbol.to_sym] || '.*'}")
end
local_segments << local_segment
segment
end.compact
raise "Invalid route: Controller not specified" unless (params.include?(:controller) || args.keys.include?(:controller))
# Create the Route instance and add it to the route collection
new_route = MyRoute.new(segments, local_segments, params, request_method, args)
@routes << new_route
new_route.arguments[:controller] ||= :controller
new_route.arguments[:action] ||= :action
# Create a tree structure for route generation
@route_structure[request_method] ||= {}
@route_structure[request_method][new_route.arguments[:controller]] ||= {}
@route_structure[request_method][new_route.arguments[:controller]][new_route.arguments[:action]] ||= []
@route_structure[request_method][new_route.arguments[:controller]][new_route.arguments[:action]] << new_route
new_route
end
def recognize(request)
# Normalize path
path = (request.path.slice(0,1) == "/" ? request.path : "/#{request.path}")
# Default to GET for request method
request_method = (request.request_method || :get)
matched = {}
matches = captures = nil
routeset = []
# Populate recognizer sets
@recognizers ||= build_recognizers
# Iterate each set of recognizers
@recognizers.each do |recognizer, routes|
# Match path to recognizer
matches = recognizer.match("#{request_method} #{path}")
# Grab set of routes + matched path
if matches && !matches.captures.compact.blank?
routeset = routes
break
end
end
raise "No route matches that path" if routeset.blank?
# Match indexes of matched path and route
if r = routeset[matches.captures.index(matches.captures.compact.first)]
# Get parameter values
params = r.params.clone
param_matches = path.scan(/#{r.local_recognizer}/).flatten
param_list = {}
r.params.each_with_index {|p,i| param_list[p] = param_matches[i]}
matched = r.arguments.merge(matched).merge(param_list)
# Default action to index
matched[:action] = 'index' if matched[:action] == :action
end
# Populate request's parameters with arguments from request + static values
request.path_parameters = matched
"#{matched[:controller].camelize}Controller".constantize
end
# TODO: I forgot what epic_fail does. Look that up.
def generate(params, recall = {}, epic_fail = nil)
# Default request method to GET
request_method = params[:method] ? params[:method].to_sym : :get
# If we're given a controller...
if params.keys.include?(:controller)
# Grab controller routes
controller_routes = @route_structure[request_method][params[:controller]]
unless controller_routes
controller_routes = @route_structure[request_method][:controller]
end
# ...then map action
action_routes = controller_routes[(params[:action] || 'index')] || controller_routes[:action]
# Find route we're looking for with the right params
action_routes.each do |route|
if (route.params - params.keys).empty?
return generate_url(route, params)
else
raise "No route to match that"
end
end
else
raise "No controller provided"
end
end
def generate_url(route, params)
route_string = route.segments.join("/")
return route_string unless route_string.include?("(.*)")
index = -1
route_string.gsub!(/\(\.\*\)/) do |match|
index += 1
params[route.params[index]].to_param
end
end
def build_recognizers
recognizers = []
current_route_set = []
current_segment = ""
@routes.each do |route|
segment = "(^#{route.request_method} #{route.recognizer}$)"
# If our recognizer is getting too big, break it up and start a new one
if ("#{current_segment}|#{segment}").length > 7730
recognizers << [/#{current_segment}/, current_route_set]
current_segment = segment
current_route_set = [route]
else
# ...otherwise keep adding to the current recognizer
current_segment = [current_segment, segment].reject{|s| s.blank?}.compact.join("|")
current_route_set << route
end
end
# Clean up any left over segments and routes
unless current_segment.blank?
recognizers << [/#{current_segment}/, current_route_set]
end
recognizers
end
end
class MyRoute
# TODO: Add dynamic attribute so we can skip parameter interpolation if there aren't any
attr_accessor :params, :segments, :arguments
attr_reader :recognizer, :request_method, :local_recognizer
def initialize(segment_list, local_segments, param_list, request_method, argument_list = {})
@segments = segment_list
@params = param_list
@arguments = argument_list || {}
@recognizer = "/#{@segments.join("\/")}"
@local_recognizer = "/#{local_segments.join("\/")}"
@request_method = request_method
end
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment