class Randy class Router def initialize(routes={}) @routes = [] @position = {} routes.each { |k, v| register k, v } end def register(route, default={}) segs = route.split("/") @routes << [segs, default] position = @position[segs] = {} segs.each_with_index { |s, i| if s =~ /\A[:*](.+)/ position[$1.to_sym] = i end } self end def route(url) @routes.each { |(route, default)| if data = route_match(route, url, default) return data end } nil end def link_to(data) @routes.sort_by { |route, default| (data.keys - @position[route].keys - default.keys).size }.each { |(route, default)| if url = generate_link(route, data.dup, default) return url end } nil end private def route_match(route, url, default={}) segs = url.split("/") data = default.dup route.each { |segment| case segment when /\A:(.*)/ if segs.empty? return nil unless data.include?($1.to_sym) else data[$1.to_sym] = segs.shift end when /\A\*(.+)/ data[$1.to_sym] = segs.dup segs.clear break when segs.first segs.shift else return nil end } if segs.empty? data else nil end end def generate_link(route, data, default) segs = route.dup additional = {} position = @position[route] default.each { |k, v| if position.include?(k) # Include explicit defaults data[k] = v unless data.include?(k) else # Remove implicit defaults data.delete k if data[k] == v end } # Is everything we need given? return nil unless (position.keys - data.keys).empty? data.each { |k, v| if position.include?(k) segs[position[k]] = v else additional[k] = v end } url = segs.flatten.join("/") unless additional.empty? url << ";" url << additional.map { |k, v| "#{cgiescape(k)}=#{cgiescape(v)}" }.join("&") end url end def cgiescape(s) s.to_s.gsub(/([^ a-zA-Z0-9_.-]+)/n) { '%'+$1.unpack('H2'*$1.size).join('%').upcase }.tr(' ', '+') end end end