require File.join(File.dirname(__FILE__), 'usher', 'node')
require File.join(File.dirname(__FILE__), 'usher', 'route')
require File.join(File.dirname(__FILE__), 'usher', 'grapher')
require File.join(File.dirname(__FILE__), 'usher', 'interface')
require File.join(File.dirname(__FILE__), 'usher', 'splitter')
require File.join(File.dirname(__FILE__), 'usher', 'exceptions')
require File.join(File.dirname(__FILE__), 'usher', 'util')
require File.join(File.dirname(__FILE__), 'usher', 'spinoffs', 'strscan_additions')
require File.join(File.dirname(__FILE__), 'usher', 'delimiters')
class Usher
attr_reader :root, :named_routes, :routes, :splitter,
:delimiters, :delimiters_regex,
:parent_route, :generator, :grapher
# Returns whether the route set is empty
#
# set = Usher.new
# set.empty? => true
# set.add_route('/test')
# set.empty? => false
def empty?
@routes.empty?
end
def route_count
@routes.size
end
# Resets the route set back to its initial state
#
# set = Usher.new
# set.add_route('/test')
# set.empty? => false
# set.reset!
# set.empty? => true
def reset!
@root = Node.root(self, request_methods)
@named_routes = {}
@routes = []
@grapher = Grapher.new
end
alias clear! reset!
# Creates a route set, with options
#
# :delimiters: Array of Strings. (default ['/', '.']). Delimiters used in path separation. Array must be single character strings.
#
# :valid_regex: String. (default '[0-9A-Za-z\$\-_\+!\*\',]+'). String that can be interpolated into regex to match
# valid character sequences within path.
#
# :request_methods: Array of Symbols. (default [:protocol, :domain, :port, :query_string, :remote_ip, :user_agent, :referer, :method, :subdomains])
# Array of methods called against the request object for the purposes of matching route requirements.
def initialize(options = nil)
self.generator = options && options.delete(:generator)
delimiters_array = options && options.delete(:delimiters) || ['/', '.']
self.delimiters = Delimiters.new(delimiters_array)
self.valid_regex = options && options.delete(:valid_regex) || '[0-9A-Za-z\$\-_\+!\*\',]+'
self.request_methods = options && options.delete(:request_methods)
reset!
end
def parser
@parser ||= Util::Parser.for_delimiters(self, valid_regex)
end
def can_generate?
!@generator.nil?
end
def generator
@generator
end
# Adds a route referencable by +name+. See add_route for format +path+ and +options+.
#
# set = Usher.new
# set.add_named_route(:test_route, '/test')
def add_named_route(name, path, options = nil)
add_route(path, options).name(name)
end
# Deletes a route referencable by +name+. At least the path and conditions have to match the route you intend to delete.
#
# set = Usher.new
# set.delete_named_route(:test_route, '/test')
def delete_named_route(name, path, options = nil)
delete_route(path, options)
@named_routes.delete(name)
end
# Attaches a +route+ to a +name+
#
# set = Usher.new
# route = set.add_route('/test')
# set.name(:test, route)
def name(name, route)
@named_routes[name] = route
route
end
# Creates a route from +path+ and +options+
#
# === +path+
# A path consists a mix of dynamic and static parts delimited by /
#
# ==== Dynamic
# Dynamic parts are prefixed with either :, *. :variable matches only one part of the path, whereas *variable can match one or
# more parts.
#
# Example:
# /path/:variable/path would match
#
# * /path/test/path
# * /path/something_else/path
# * /path/one_more/path
#
# In the above examples, 'test', 'something_else' and 'one_more' respectively would be bound to the key :variable.
# However, /path/test/one_more/path would not be matched.
#
# Example:
# /path/*variable/path would match
#
# * /path/one/two/three/path
# * /path/four/five/path
#
# In the above examples, ['one', 'two', 'three'] and ['four', 'five'] respectively would be bound to the key :variable.
#
# As well, variables can have a regex matcher.
#
# Example:
# /product/{:id,\d+} would match
#
# * /product/123
# * /product/4521
#
# But not
# * /product/AE-35
#
# As well, the same logic applies for * variables as well, where only parts matchable by the supplied regex will
# actually be bound to the variable
#
# Variables can also have a greedy regex matcher. These matchers ignore all delimiters, and continue matching for as long as much as their
# regex allows.
#
# Example:
# /product/{!id,hello/world|hello} would match
#
# * /product/hello/world
# * /product/hello
#
# ==== Static
#
# Static parts of literal character sequences. For instance, /path/something.html would match only the same path.
# As well, static parts can have a regex pattern in them as well, such as /path/something.{html|xml} which would match only
# /path/something.html and /path/something.xml
#
# ==== Optional sections
#
# Sections of a route can be marked as optional by surrounding it with brackets. For instance, in the above static example, /path/something(.html) would match both /path/something and /path/something.html.
#
# ==== One and only one sections
#
# Sections of a route can be marked as "one and only one" by surrounding it with brackets and separating parts of the route with pipes.
# For instance, the path, /path/something(.xml|.html) would only match /path/something.xml and
# /path/something.html. Generally its more efficent to use one and only sections over using regex.
#
# === +options+
# * +requirements+ - After transformation, tests the condition using ===. If it returns false, it raises an Usher::ValidationException
# * +conditions+ - Accepts any of the +request_methods+ specificied in the construction of Usher. This can be either a string or a regular expression.
# * Any other key is interpreted as a requirement for the variable of its name.
def add_route(path, options = nil)
route = get_route(path, options)
@root.add(route)
@routes << route
@grapher.add_route(route)
route.parent_route = parent_route if parent_route
route
end
# Deletes a route. At least the path and conditions have to match the route you intend to delete.
#
# set = Usher.new
# set.delete_route('/test')
def delete_route(path, options = nil)
route = get_route(path, options)
@root.delete(route)
@routes = @root.unique_routes
rebuild_grapher!
route
end
# Recognizes a +request+ and returns +nil+ or an Usher::Node::Response, which is a struct containing a Usher::Route::Path and an array of arrays containing the extracted parameters.
#
# Request = Struct.new(:path)
# set = Usher.new
# route = set.add_route('/test')
# set.recognize(Request.new('/test')).path.route == route => true
def recognize(request, path = request.path)
@root.find(self, request, path, @splitter.url_split(path))
end
# Recognizes a +path+ and returns +nil+ or an Usher::Node::Response, which is a struct containing a Usher::Route::Path and an array of arrays containing the extracted parameters. Convenience method for when recognizing on the request object is unneeded.
#
# Request = Struct.new(:path)
# set = Usher.new
# route = set.add_route('/test')
# set.recognize_path('/test').path.route == route => true
def recognize_path(path)
recognize(nil, path)
end
# Recognizes a set of +parameters+ and gets the closest matching Usher::Route::Path or +nil+ if no route exists.
#
# set = Usher.new
# route = set.add_route('/:controller/:action')
# set.path_for_options({:controller => 'test', :action => 'action'}) == path.route => true
def path_for_options(options)
@grapher.find_matching_path(options)
end
def parent_route=(route)
@parent_route = route
routes.each{|r| r.parent_route = route}
end
def dup
replacement = super
original = self
inverted_named_routes = original.named_routes.invert
replacement.instance_eval do
@parser = nil
reset!
original.routes.each do |route|
new_route = route.dup
new_route.router = self
@root.add(new_route)
@routes << new_route
if name = inverted_named_routes[route]
@named_routes[name] = new_route
end
end
send(:generator=, original.generator.class.new) if original.can_generate?
rebuild_grapher!
end
replacement
end
private
attr_accessor :request_methods
attr_reader :valid_regex
def generator=(generator)
if generator
@generator = generator
@generator.usher = self
end
@generator
end
def delimiters=(delimiters)
@delimiters = delimiters
@delimiters_regex = @delimiters.collect{|d| Regexp.quote(d)} * '|'
@delimiters
end
def valid_regex=(valid_regex)
@valid_regex = valid_regex
@splitter = Splitter.for_delimiters(self.delimiters)
@valid_regex
end
def get_route(path, options = nil)
conditions = options && options.delete(:conditions) || nil
requirements = options && options.delete(:requirements) || nil
default_values = options && options.delete(:default_values) || nil
generate_with = options && options.delete(:generate_with) || nil
if options
options.delete_if do |k, v|
if v.is_a?(Regexp) || v.is_a?(Proc)
(requirements ||= {})[k] = v
true
end
end
end
if conditions
conditions.keys.all?{|k| request_methods.include?(k)} or raise
end
route = parser.generate_route(path, conditions, requirements, default_values, generate_with)
route.to(options) if options && !options.empty?
route
end
def rebuild_grapher!
@grapher = Grapher.new
@routes.each{|r| @grapher.add_route(r)}
end
end