Permalink
Browse files

generator class

  • Loading branch information...
1 parent a216cc0 commit 669a5a6dc98756970e58052be12681f7a0e03635 @joshbuddy committed Sep 13, 2011
View
@@ -7,6 +7,7 @@
require 'http_router/request'
require 'http_router/response'
require 'http_router/route'
+require 'http_router/generator'
require 'http_router/route_proxy'
require 'http_router/regex_route_generation'
require 'http_router/rack'
@@ -25,7 +26,7 @@ class HttpRouter
LeftOverOptions = Class.new(RuntimeError)
attr_reader :root, :routes, :known_methods, :named_routes, :nodes
- attr_accessor :default_app, :url_mount, :route_class
+ attr_accessor :default_app, :url_mount, :route_class, :default_host, :default_port, :default_scheme
# Creates a new HttpRouter.
# Can be called with either <tt>HttpRouter.new(proc{|env| ... }, { .. options .. })</tt> or with the first argument omitted.
@@ -133,6 +134,7 @@ def reset!
uncompile
@routes, @named_routes, @root = [], Hash.new{|h,k| h[k] = []}, Node::Root.new(self)
@default_app = Proc.new{ |env| ::Rack::Response.new("Your request couldn't be found", 404).finish }
+ @default_host, @default_port, @default_scheme = 'localhost', 80, 'http'
end
# Assigns the default application.
@@ -160,6 +162,12 @@ def url(route, *args)
end
alias_method :compiling_url, :url
+ def path(route, *args)
+ compile
+ path(route, *args)
+ end
+ alias_method :compiling_path, :path
+
# This method is invoked when a Path object gets called with an env. Override it to implement custom path processing.
def process_destination_path(path, env)
path.route.dest.call(env)
@@ -228,15 +236,20 @@ def compile
return if @compiled
@root.compile(@routes)
@named_routes.each do |_, routes|
- routes.sort!{|r1, r2| r2.significant_variable_names.size <=> r1.significant_variable_names.size }
+ routes.sort!{|r1, r2| r2.max_param_count <=> r1.max_param_count }
end
- instance_eval "undef :url; alias :url :raw_url; undef :call; alias :call :raw_call", __FILE__, __LINE__
+
+ instance_eval "undef :path; alias :path :raw_path;
+ undef :url; alias :url :raw_url;
+ undef :call; alias :call :raw_call", __FILE__, __LINE__
@compiled = true
end
def uncompile
return unless @compiled
- instance_eval "undef :url; alias :url :compiling_url; undef :call; alias :call :compiling_call", __FILE__, __LINE__
+ instance_eval "undef :path; alias :path :compiling_path;
+ undef :url; alias :url :compiling_url;
+ undef :call; alias :call :compiling_call", __FILE__, __LINE__
@root.uncompile
@compiled = false
end
@@ -249,6 +262,14 @@ def raw_url(route, *args)
raise(InvalidRouteException)
end
+ def raw_path(route, *args)
+ case route
+ when Symbol then @named_routes.key?(route) && @named_routes[route].each{|r| path = r.path(*args); return path if path}
+ when Route then return route.path(*args)
+ end
+ raise(InvalidRouteException)
+ end
+
def raw_call(env, perform_call = true)
rack_request = ::Rack::Request.new(env)
request = Request.new(rack_request.path_info, rack_request, perform_call)
@@ -0,0 +1,149 @@
+class HttpRouter
+ class Generator
+ SCHEME_PORTS = {'http' => 80, 'https' => 443}
+
+ class PathGenerator
+ attr_reader :path, :param_names
+ def initialize(route, path, validation_regex = nil)
+ @route = route
+ @path = path.dup
+ @param_names = if path.respond_to?(:names)
+ path.names.map(&:to_sym)
+ elsif path.is_a?(String)
+ path.scan(/(^|[^\\])[:\*]([a-zA-Z0-9_]+)/).map{|p| p.last.to_sym}
+ else
+ []
+ end
+
+ if path.is_a?(String)
+ path[0, 0] = '/' unless path[0] == ?/
+ regex_parts = path.split(/([:\*][a-zA-Z0-9_]+)/)
+ regex, code = '', ''
+ dynamic = false
+ regex_parts.each_with_index do |part, index|
+ case part[0]
+ when ?:, ?*
+ if index != 0 && regex_parts[index - 1][-1] == ?\\
+ regex << Regexp.quote(part) unless validation_regex
+ code << part
+ dynamic = true
+ else
+ regex << (@route.matches_with(part[1, part.size].to_sym) || '.*?').to_s unless validation_regex
+ code << "\#{args.shift || (options && options.delete(:#{part[1, part.size]})) || return}"
+ dynamic = true
+ end
+ else
+ regex << Regexp.quote(part) unless validation_regex
+ code << part
+ end
+ end
+ validation_regex ||= Regexp.new("^#{regex}$") if dynamic
+ if validation_regex
+ instance_eval <<-EOT, __FILE__, __LINE__ + 1
+ def generate(args, options)
+ generated_path = \"#{code}\"
+ #{validation_regex.inspect}.match(generated_path) ? generated_path : nil
+ end
+ EOT
+ else
+ instance_eval <<-EOT, __FILE__, __LINE__ + 1
+ def generate(args, options)
+ \"#{code}\"
+ end
+ EOT
+ end
+ end
+ end
+ end
+
+ attr_reader :param_names
+ def initialize(route, paths)
+ @route, @paths = route, paths
+ @router = @route.router
+ @route.generator = self
+ @path_generators = @paths.map do |p|
+ generator = PathGenerator.new(route, p.is_a?(String) ? p : route.path_for_generation, p.is_a?(Regexp) ? p : nil)
+ end
+ end
+
+ def max_param_count
+ @max_param_count ||= @path_generators.map{|p| p.param_names.size}.max
+ end
+
+ def each_path
+ @path_generators.each {|p| yield p.path, p.param_names}
+ end
+
+ def full_url(*args)
+ "#{scheme_port.first}#{url(*args)}"
+ end
+
+ def url(*args)
+ "://#{@route.host || @router.default_host}#{scheme_port.last}#{path(*args)}"
+ end
+
+ def path(*args)
+ result, extra_params = path_with_params(*args)
+ append_querystring(result, extra_params)
+ end
+
+ private
+ def scheme_port
+ @scheme_port ||= begin
+ scheme = @route.scheme || @router.default_scheme
+ port = @router.default_port
+ port_part = SCHEME_PORTS.key?(scheme) && SCHEME_PORTS[scheme] == port ? '' : ":#{port}"
+ [scheme, port_part]
+ end
+ end
+
+ def path_with_params(*a)
+ path_args_processing(a) do |args, options|
+ path = args.empty? ? matching_path(options) : matching_path(args, options)
+ path &&= path.generate(args, options)
+ raise TooManyParametersException unless args.empty?
+ raise InvalidRouteException.new("Error generating #{@route.path_for_generation}") unless path
+ path ? [path, options] : nil
+ end
+ end
+
+ def path_args_processing(args)
+ options = args.last.is_a?(Hash) ? args.pop : nil
+ options = options.nil? ? @route.default_values.dup : @route.default_values.merge(options) if @route.default_values
+ options.delete_if{ |k,v| v.nil? } if options
+ result, params = yield args, options
+ mount_point = @router.url_mount && (options ? @router.url_mount.url(options) : @router.url_mount.url)
+ mount_point ? [File.join(mount_point, result), params] : [result, params]
+ end
+
+ def matching_path(params, other_hash = nil)
+ return @path_generators.first if @path_generators.size == 1
+ case params
+ when Array, nil
+ significant_keys = other_hash && param_names & other_hash.keys
+ @path_generators.find { |path|
+ params_size = params ? params.size : 0
+ path.param_names.size == (significant_keys ? (params_size) + significant_keys.size : params_size) }
+ when Hash
+ @path_generators.find { |path| (params && !params.empty? && (path.param_names & params.keys).size == path.param_names.size) || path.param_names.empty? }
+ end
+ end
+
+ def append_querystring_value(uri, key, value)
+ case value
+ when Array then value.each{ |v| append_querystring_value(uri, "#{key}[]", v) }
+ when Hash then value.each{ |k, v| append_querystring_value(uri, "#{key}[#{k}]", v) }
+ else uri << "&#{CGI.escape(key.to_s)}=#{CGI.escape(value.to_s)}"
+ end
+ end
+
+ def append_querystring(uri, params)
+ if params && !params.empty?
+ uri_size = uri.size
+ params.each{ |k,v| append_querystring_value(uri, k, v) }
+ uri[uri_size] = ??
+ end
+ uri
+ end
+ end
+end
@@ -7,7 +7,6 @@ def initialize(router, parent, route, path, param_names = [])
@route, @path, @param_names, @dynamic = route, path, param_names, !param_names.empty?
@route.add_path(self)
raise AmbiguousVariableException, "You have duplicate variable name present: #{param_names.join(', ')}" if param_names.uniq.size != param_names.size
- Util.add_path_generation(self, route, @path) if @path.respond_to?(:split)
super router, parent
router.uncompile
end
@@ -16,13 +15,6 @@ def hashify_params(params)
@dynamic && params ? Hash[param_names.zip(params)] : {}
end
- def url(args, options)
- if path = raw_url(args, options)
- raise TooManyParametersException.new("for #{path.inspect} with #{args.inspect}") unless args.empty?
- [URI.escape(path), options]
- end
- end
-
def to_code
path_ivar = inject_root_ivar(self)
"#{"if request.path_finished?" unless route.match_partially}
@@ -57,11 +49,6 @@ def usable?(other)
def inspect_label
"Path: #{path.inspect} for route #{route.name || 'unnamed route'} to #{route.dest.inspect}"
end
-
- private
- def raw_url(args, options)
- raise InvalidRouteException.new("for #{@path}")
- end
end
end
end
@@ -39,15 +39,15 @@ def compile(routes)
private
def add_route(route)
- paths = if route.path.nil?
+ paths = if route.path_for_generation.nil?
route.match_partially = true
[]
- elsif route.path.is_a?(Regexp)
- [route.path]
+ elsif route.path_for_generation.is_a?(Regexp)
+ [route.path_for_generation]
else
- path_for_processing = route.path.dup
+ path_for_generation = route.path_for_generation.dup
start_index, end_index = 0, 1
- raw_paths, chars = [""], route.path.split('')
+ raw_paths, chars = [""], path_for_generation.split('')
until chars.empty?
case fc = chars.first[0]
when ?(
@@ -69,44 +69,38 @@ def add_route(route)
if paths.empty?
add_non_path_to_tree(route, @router.root, nil, [])
else
- paths.each do |path|
+ Generator.new(route, paths).each_path do |path, param_names|
case path
when Regexp
- param_names = path.respond_to?(:names) ? path.names.map(&:to_sym) : []
- Util.add_path_generation(route, route, route.path_for_generation, path) if route.path_for_generation
- route.instance_eval "extend RegexRouteGeneration", __FILE__, __LINE__
add_non_path_to_tree(route, add_free_match(path), path, param_names)
else
- param_names = []
node = self
path.split(/\//).each do |part|
next if part == ''
parts = part.scan(/\\.|[:*][a-z0-9_]+|[^:*\\]+/)
- node = parts.size == 1 ? add_normal_part(route, node, part, param_names) : add_complex_part(route, node, parts, param_names)
+ node = parts.size == 1 ? add_normal_part(route, node, part) : add_complex_part(route, node, parts)
end
add_non_path_to_tree(route, node, path, param_names)
end
end
end
end
- def add_normal_part(route, node, part, param_names)
+ def add_normal_part(route, node, part)
name = part[1, part.size]
node = case part[0]
when ?\\
node.add_lookup(part[1].chr)
when ?:
- param_names << name.to_sym
route.matches_with(name) ? node.add_spanning_match(route.matches_with(name)) : node.add_variable
when ?*
- param_names << name.to_sym
route.matches_with(name) ? node.add_glob_regexp(route.matches_with(name)) : node.add_glob
else
node.add_lookup(part)
end
end
- def add_complex_part(route, node, parts, param_names)
+ def add_complex_part(route, node, parts)
capturing_indicies, splitting_indicies, captures, spans = [], [], 0, false
regex = parts.inject('') do |reg, part|
reg << case part[0]
@@ -116,7 +110,6 @@ def add_complex_part(route, node, parts, param_names)
captures += 1
(part[0] == ?* ? splitting_indicies : capturing_indicies) << captures
name = part[1, part.size].to_sym
- param_names << name
if spans
route.matches_with(name) ? "((?:#{route.matches_with(name)}\\/?)+)" : '(.*?)'
else
@@ -130,12 +123,12 @@ def add_complex_part(route, node, parts, param_names)
node.add_match(Regexp.new("#{regex}$"), capturing_indicies, splitting_indicies)
end
- def add_non_path_to_tree(route, node, path, names)
+ def add_non_path_to_tree(route, node, path, param_names)
node = node.add_host([route.host, route.other_hosts].flatten.compact) if route.host or route.other_hosts
node = node.add_user_agent(route.user_agent) if route.user_agent
node = node.add_request_method(route.request_methods) if route.request_methods
node = node.add_scheme(route.schemes) if route.schemes
- path_obj = node.add_destination(route, path, names)
+ path_obj = node.add_destination(route, path, param_names)
path_obj
end
Oops, something went wrong.

0 comments on commit 669a5a6

Please sign in to comment.