usher 0.5.13 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +16 -0
- data/Rakefile +1 -1
- data/VERSION.yml +3 -3
- data/lib/usher.rb +31 -9
- data/lib/usher/delimiters.rb +16 -14
- data/lib/usher/grapher.rb +45 -22
- data/lib/usher/interface.rb +8 -4
- data/lib/usher/interface/sinatra.rb +66 -0
- data/lib/usher/node.rb +9 -1
- data/lib/usher/route.rb +45 -12
- data/lib/usher/route/path.rb +11 -2
- data/lib/usher/route/static.rb +1 -1
- data/lib/usher/util/generate.rb +6 -4
- data/lib/usher/util/parser.rb +133 -142
- data/spec/private/delimiters_spec.rb +5 -5
- data/spec/private/destination_spec.rb +32 -0
- data/spec/private/generate_spec.rb +28 -0
- data/spec/private/generate_with_spec.rb +2 -2
- data/spec/private/rack/dispatch_spec.rb +38 -1
- data/spec/private/recognize_spec.rb +27 -0
- data/spec/private/url_parts_spec.rb +4 -4
- metadata +6 -6
- data/lib/usher/spinoffs/strscan_additions.rb +0 -31
- data/spec/private/string_scanner_spec.rb +0 -39
data/README.rdoc
CHANGED
@@ -89,6 +89,7 @@ For instance, the path, <tt>/path/something(.xml|.html)</tt> would only match <t
|
|
89
89
|
* +requirements+ - After transformation, tests the condition using ===. If it returns false, it raises an <tt>Usher::ValidationException</tt>
|
90
90
|
* +conditions+ - Accepts any of the +request_methods+ specificied in the construction of Usher. This can be either a <tt>string</tt> or a regular expression.
|
91
91
|
* +default_values+ - Provides values for variables in your route for generation. If you're using URL generation, then any values supplied here that aren't included in your path will be appended to the query string.
|
92
|
+
* +priority+ - If there are two routes which equally match, the route with the highest priority will match first.
|
92
93
|
* Any other key is interpreted as a requirement for the variable of its name.
|
93
94
|
|
94
95
|
== Rails
|
@@ -123,6 +124,21 @@ For instance, the path, <tt>/path/something(.xml|.html)</tt> would only match <t
|
|
123
124
|
>> curl http://127.0.0.1:3000/hello/samueltanders
|
124
125
|
<< Hi there samueltanders
|
125
126
|
|
127
|
+
|
128
|
+
== Sinatra
|
129
|
+
|
130
|
+
In Sinatra, you get the extra method, +generate+, which lets you generate a url. Name your routes with <tt>:name</tt> when you define them.
|
131
|
+
|
132
|
+
require 'rubygems'
|
133
|
+
require 'usher'
|
134
|
+
require 'sinatra'
|
135
|
+
|
136
|
+
Usher::Interface.for(:sinatra)
|
137
|
+
|
138
|
+
get '/hi', :name => :hi do
|
139
|
+
"Hello World! #{generate(:hi)}"
|
140
|
+
end
|
141
|
+
|
126
142
|
== DONE
|
127
143
|
|
128
144
|
* add support for () optional parts
|
data/Rakefile
CHANGED
@@ -11,7 +11,7 @@ begin
|
|
11
11
|
s.homepage = "http://github.com/joshbuddy/usher"
|
12
12
|
s.authors = ["Joshua Hull", 'Jakub Šťastný', 'Daniel Neighman', 'Daniel Vartanov'].sort
|
13
13
|
s.files = FileList["[A-Z]*", "{lib,spec,rails}/**/*"]
|
14
|
-
s.add_dependency 'fuzzyhash', '>=0.0.
|
14
|
+
s.add_dependency 'fuzzyhash', '>=0.0.11'
|
15
15
|
s.rubyforge_project = 'joshbuddy-usher'
|
16
16
|
end
|
17
17
|
Jeweler::GemcutterTasks.new
|
data/VERSION.yml
CHANGED
data/lib/usher.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
|
1
|
+
$LOAD_PATH << File.expand_path(File.dirname(__FILE__))
|
2
2
|
require File.join('usher', 'node')
|
3
3
|
require File.join('usher', 'route')
|
4
4
|
require File.join('usher', 'grapher')
|
@@ -6,7 +6,6 @@ require File.join('usher', 'interface')
|
|
6
6
|
require File.join('usher', 'splitter')
|
7
7
|
require File.join('usher', 'exceptions')
|
8
8
|
require File.join('usher', 'util')
|
9
|
-
require File.join('usher', 'spinoffs', 'strscan_additions')
|
10
9
|
require File.join('usher', 'delimiters')
|
11
10
|
|
12
11
|
class Usher
|
@@ -26,6 +25,8 @@ class Usher
|
|
26
25
|
@routes.empty?
|
27
26
|
end
|
28
27
|
|
28
|
+
# Returns the number of routes
|
29
|
+
#
|
29
30
|
def route_count
|
30
31
|
@routes.size
|
31
32
|
end
|
@@ -41,7 +42,7 @@ class Usher
|
|
41
42
|
@root = Node.root(self, request_methods)
|
42
43
|
@named_routes = {}
|
43
44
|
@routes = []
|
44
|
-
@grapher = Grapher.new
|
45
|
+
@grapher = Grapher.new(self)
|
45
46
|
@priority_lookups = false
|
46
47
|
end
|
47
48
|
alias clear! reset!
|
@@ -55,14 +56,34 @@ class Usher
|
|
55
56
|
#
|
56
57
|
# <tt>:request_methods</tt>: Array of Symbols. (default <tt>[:protocol, :domain, :port, :query_string, :remote_ip, :user_agent, :referer, :method, :subdomains]</tt>)
|
57
58
|
# Array of methods called against the request object for the purposes of matching route requirements.
|
59
|
+
#
|
60
|
+
# <tt>:generator</tt>: +nil+ or Generator instance. (default: +nil+) Take a look at <tt>Usher::Util::Generators for examples.</tt>.
|
61
|
+
#
|
62
|
+
# <tt>:ignore_trailing_delimiters</tt>: +true+ or +false+. (default: +false+) Ignore trailing delimiters in recognizing paths.
|
63
|
+
#
|
64
|
+
# <tt>:consider_destination_keys</tt>: +true+ or +false+. (default: +false+) When generating, and using hash destinations, you can have
|
65
|
+
# Usher use the destination hash to match incoming params.
|
66
|
+
#
|
67
|
+
# Example, you create a route with a destination of :controller => 'test', :action => 'action'. If you made a call to generator with :controller => 'test',
|
68
|
+
# :action => 'action', it would pick that route to use for generation.
|
58
69
|
def initialize(options = nil)
|
59
|
-
self.generator
|
60
|
-
self.delimiters
|
61
|
-
self.valid_regex
|
62
|
-
self.request_methods
|
70
|
+
self.generator = options && options.delete(:generator)
|
71
|
+
self.delimiters = Delimiters.new(options && options.delete(:delimiters) || ['/', '.'])
|
72
|
+
self.valid_regex = options && options.delete(:valid_regex) || '[0-9A-Za-z\$\-_\+!\*\',]+'
|
73
|
+
self.request_methods = options && options.delete(:request_methods)
|
74
|
+
self.ignore_trailing_delimiters = options && options.key?(:ignore_trailing_delimiters) ? options.delete(:ignore_trailing_delimiters) : false
|
75
|
+
self.consider_destination_keys = options && options.key?(:consider_destination_keys) ? options.delete(:consider_destination_keys) : false
|
63
76
|
reset!
|
64
77
|
end
|
65
78
|
|
79
|
+
def ignore_trailing_delimiters?
|
80
|
+
@ignore_trailing_delimiters
|
81
|
+
end
|
82
|
+
|
83
|
+
def consider_destination_keys?
|
84
|
+
@consider_destination_keys
|
85
|
+
end
|
86
|
+
|
66
87
|
def parser
|
67
88
|
@parser ||= Util::Parser.for_delimiters(self, valid_regex)
|
68
89
|
end
|
@@ -172,6 +193,7 @@ class Usher
|
|
172
193
|
# * +requirements+ - After transformation, tests the condition using ===. If it returns false, it raises an <tt>Usher::ValidationException</tt>
|
173
194
|
# * +conditions+ - Accepts any of the +request_methods+ specificied in the construction of Usher. This can be either a <tt>string</tt> or a regular expression.
|
174
195
|
# * +default_values+ - Provides values for variables in your route for generation. If you're using URL generation, then any values supplied here that aren't included in your path will be appended to the query string.
|
196
|
+
# * +priority+ - If there are two routes which equally match, the route with the highest priority will match first.
|
175
197
|
# * Any other key is interpreted as a requirement for the variable of its name.
|
176
198
|
def add_route(path, options = nil)
|
177
199
|
route = get_route(path, options)
|
@@ -252,7 +274,7 @@ class Usher
|
|
252
274
|
|
253
275
|
private
|
254
276
|
|
255
|
-
attr_accessor :request_methods
|
277
|
+
attr_accessor :request_methods, :ignore_trailing_delimiters, :consider_destination_keys
|
256
278
|
attr_reader :valid_regex
|
257
279
|
|
258
280
|
def generator=(generator)
|
@@ -308,7 +330,7 @@ class Usher
|
|
308
330
|
end
|
309
331
|
|
310
332
|
def rebuild_grapher!
|
311
|
-
@grapher = Grapher.new
|
333
|
+
@grapher = Grapher.new(self)
|
312
334
|
@routes.each{|r| @grapher.add_route(r)}
|
313
335
|
end
|
314
336
|
end
|
data/lib/usher/delimiters.rb
CHANGED
@@ -1,22 +1,24 @@
|
|
1
|
-
class
|
1
|
+
class Usher
|
2
|
+
class Delimiters < Array
|
2
3
|
|
3
|
-
|
4
|
+
attr_reader :unescaped
|
4
5
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
6
|
+
def initialize(ary)
|
7
|
+
super ary
|
8
|
+
@unescaped = self.map do |delimiter|
|
9
|
+
(delimiter[0] == ?\\) ? delimiter[1..-1] : delimiter
|
10
|
+
end
|
9
11
|
end
|
10
|
-
end
|
11
12
|
|
12
|
-
|
13
|
-
|
13
|
+
def first_in(array)
|
14
|
+
# TODO: should we optimize this O(n*m)? hash or modified or KNP or at leaset sort + b-search. But they are so short
|
14
15
|
|
15
|
-
|
16
|
-
|
16
|
+
array.each do |element|
|
17
|
+
return element if self.unescaped.any? { |delimiter| delimiter == element }
|
18
|
+
end
|
19
|
+
nil
|
17
20
|
end
|
18
|
-
nil
|
19
|
-
end
|
20
21
|
|
21
|
-
|
22
|
+
# TODO: Delimiters#regex and so on
|
23
|
+
end
|
22
24
|
end
|
data/lib/usher/grapher.rb
CHANGED
@@ -1,7 +1,10 @@
|
|
1
1
|
class Usher
|
2
2
|
class Grapher
|
3
3
|
|
4
|
-
|
4
|
+
attr_reader :routes, :router, :orders, :key_count, :cache
|
5
|
+
|
6
|
+
def initialize(router)
|
7
|
+
@router = router
|
5
8
|
reset!
|
6
9
|
end
|
7
10
|
|
@@ -10,49 +13,69 @@ class Usher
|
|
10
13
|
@orders = Hash.new{|h,k| h[k] = Hash.new{|h2, k2| h2[k2] = []}}
|
11
14
|
@key_count = Hash.new(0)
|
12
15
|
@cache = {}
|
16
|
+
@routes = []
|
13
17
|
end
|
14
18
|
|
15
19
|
def add_route(route)
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
20
|
+
routes << route
|
21
|
+
end
|
22
|
+
|
23
|
+
def process_routes
|
24
|
+
return if @processed
|
25
|
+
routes.each do |route|
|
26
|
+
route.paths.each do |path|
|
27
|
+
if path.dynamic?
|
28
|
+
path.dynamic_keys.each do |k|
|
29
|
+
orders[path.dynamic_keys.size][k] << path
|
30
|
+
key_count[k] += 1
|
31
|
+
end
|
32
|
+
|
33
|
+
dynamic_parts_with_defaults = path.dynamic_parts.select{|part| part.default_value }.map{|dp| dp.name}
|
34
|
+
dynamic_parts_without_defaults = path.dynamic_parts.select{|part| !part.default_value }.map{|dp| dp.name}
|
25
35
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
36
|
+
(1...(2 ** (dynamic_parts_with_defaults.size))).each do |i|
|
37
|
+
current_set = dynamic_parts_without_defaults.dup
|
38
|
+
dynamic_parts_with_defaults.each_with_index do |dp, index|
|
39
|
+
current_set << dp unless (index & i) == 0
|
40
|
+
end
|
41
|
+
|
42
|
+
current_set.each do |k|
|
43
|
+
orders[current_set.size][k] << path
|
44
|
+
key_count[k] += 1
|
45
|
+
end
|
30
46
|
end
|
31
47
|
|
32
|
-
|
33
|
-
|
34
|
-
|
48
|
+
end
|
49
|
+
|
50
|
+
if router.consider_destination_keys?
|
51
|
+
path.route.destination_keys.each do |k|
|
52
|
+
orders[path.route.destination_keys.size][k] << path
|
53
|
+
key_count[k] += 1
|
35
54
|
end
|
36
55
|
end
|
37
56
|
end
|
38
57
|
end
|
58
|
+
@processed = true
|
39
59
|
end
|
40
60
|
|
41
61
|
def significant_keys
|
42
|
-
@significant_keys ||=
|
62
|
+
@significant_keys ||= key_count.keys.uniq
|
43
63
|
end
|
44
64
|
|
45
65
|
def find_matching_path(params)
|
46
66
|
unless params.empty?
|
67
|
+
process_routes
|
47
68
|
set = params.keys & significant_keys
|
48
|
-
if cached =
|
69
|
+
if cached = cache[set]
|
49
70
|
return cached
|
50
71
|
end
|
51
72
|
set.size.downto(1) do |o|
|
52
73
|
set.each do |k|
|
53
|
-
|
54
|
-
if r.
|
55
|
-
|
74
|
+
orders[o][k].each do |r|
|
75
|
+
if r.can_generate_from_keys?(set)
|
76
|
+
cache[set] = r
|
77
|
+
return r
|
78
|
+
elsif router.consider_destination_keys? && r.can_generate_from_params?(params)
|
56
79
|
return r
|
57
80
|
end
|
58
81
|
end
|
data/lib/usher/interface.rb
CHANGED
@@ -15,18 +15,22 @@ class Usher
|
|
15
15
|
register(:rack, File.join(File.dirname(__FILE__), 'interface', 'rack'))
|
16
16
|
register(:rails3, File.join(File.dirname(__FILE__), 'interface', 'rails3'))
|
17
17
|
register(:text, File.join(File.dirname(__FILE__), 'interface', 'text'))
|
18
|
+
register(:sinatra, File.join(File.dirname(__FILE__), 'interface', 'sinatra'))
|
18
19
|
|
19
|
-
|
20
|
-
def self.for(name, &block)
|
20
|
+
def self.class_for(name)
|
21
21
|
name = name.to_sym
|
22
22
|
if InterfaceRegistry[name]
|
23
23
|
require InterfaceRegistry[name]
|
24
|
-
|
25
|
-
const.new(&block)
|
24
|
+
Usher::Interface.const_get(File.basename(InterfaceRegistry[name]).to_s.split(/_/).map{|e| e.capitalize}.join)
|
26
25
|
else
|
27
26
|
raise ArgumentError, "Interface #{name.inspect} doesn't exist. Choose one of: #{InterfaceRegistry.keys.inspect}"
|
28
27
|
end
|
29
28
|
end
|
30
29
|
|
30
|
+
# Usher::Interface.for(:rack, &block)
|
31
|
+
def self.for(name, *args, &block)
|
32
|
+
class_for(name).new(*args, &block)
|
33
|
+
end
|
34
|
+
|
31
35
|
end
|
32
36
|
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
class Usher
|
2
|
+
module Interface
|
3
|
+
class Sinatra
|
4
|
+
|
5
|
+
module Extension
|
6
|
+
|
7
|
+
def self.included(cls)
|
8
|
+
cls::Base.class_eval(<<-HERE_DOC, __FILE__, __LINE__)
|
9
|
+
def self.route(verb, path, options={}, &block)
|
10
|
+
@router ||= Usher.new(:request_methods => [:request_method, :host, :port, :scheme], :generator => Usher::Util::Generators::URL.new)
|
11
|
+
|
12
|
+
name = options.delete(:name)
|
13
|
+
options[:conditions] ||= {}
|
14
|
+
options[:conditions][:request_method] = verb
|
15
|
+
options[:conditions][:host] = options.delete(:host) if options.key?(:host)
|
16
|
+
|
17
|
+
# Because of self.options.host
|
18
|
+
host_name(options.delete(:host)) if options.key?(:host)
|
19
|
+
|
20
|
+
define_method "\#{verb} \#{path}", &block
|
21
|
+
unbound_method = instance_method("\#{verb} \#{path}")
|
22
|
+
block =
|
23
|
+
if block.arity != 0
|
24
|
+
lambda { unbound_method.bind(self).call(*@block_params) }
|
25
|
+
else
|
26
|
+
lambda { unbound_method.bind(self).call }
|
27
|
+
end
|
28
|
+
|
29
|
+
invoke_hook(:route_added, verb, path, block)
|
30
|
+
|
31
|
+
route = @router.add_route(path, options).to(block)
|
32
|
+
route.name(name) if name
|
33
|
+
route
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.router
|
37
|
+
@router
|
38
|
+
end
|
39
|
+
|
40
|
+
def route!(base = self.class)
|
41
|
+
if self.class.router and match = self.class.router.recognize(@request)
|
42
|
+
@block_params = match.params.map{|p| p.last}
|
43
|
+
@params = @params ? @params.merge(match.params_as_hash) : match.params_as_hash
|
44
|
+
route_eval(&match.destination)
|
45
|
+
elsif base.superclass.respond_to?(:routes)
|
46
|
+
route! base.superclass
|
47
|
+
else
|
48
|
+
route_missing
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def generate(name, *params)
|
53
|
+
self.class.router.generator.generate(name, *params)
|
54
|
+
end
|
55
|
+
|
56
|
+
HERE_DOC
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
|
61
|
+
def initialize
|
62
|
+
::Sinatra.send(:include, Extension)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
data/lib/usher/node.rb
CHANGED
@@ -8,6 +8,14 @@ class Usher
|
|
8
8
|
def partial_match?
|
9
9
|
!remaining_path.nil?
|
10
10
|
end
|
11
|
+
|
12
|
+
def params_as_hash
|
13
|
+
params.inject({}){|hash, pair| hash[pair.first] = pair.last; hash}
|
14
|
+
end
|
15
|
+
|
16
|
+
def destination
|
17
|
+
path && path.route.destination
|
18
|
+
end
|
11
19
|
end
|
12
20
|
|
13
21
|
attr_reader :normal, :greedy, :request
|
@@ -112,7 +120,7 @@ class Usher
|
|
112
120
|
end
|
113
121
|
|
114
122
|
def find(usher, request_object, original_path, path, params = [], position = 0)
|
115
|
-
if terminates? && (path.empty? || terminates.route.partial_match?)
|
123
|
+
if terminates? && (path.empty? || terminates.route.partial_match? || (usher.ignore_trailing_delimiters? && path.all?{|p| usher.delimiters.include?(p)}))
|
116
124
|
terminates.route.partial_match? ?
|
117
125
|
Response.new(terminates, params, original_path[position, original_path.size], original_path[0, position]) :
|
118
126
|
Response.new(terminates, params, nil, original_path)
|
data/lib/usher/route.rb
CHANGED
@@ -22,9 +22,20 @@ class Usher
|
|
22
22
|
@generate_with = GenerateWith.new(generate_with[:scheme], generate_with[:port], generate_with[:host]) if generate_with
|
23
23
|
end
|
24
24
|
|
25
|
+
def destination_keys
|
26
|
+
@destination_keys ||= case
|
27
|
+
when Hash
|
28
|
+
destination.keys
|
29
|
+
when CompoundDestination
|
30
|
+
destination.options.keys
|
31
|
+
else
|
32
|
+
nil
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
25
36
|
def grapher
|
26
37
|
unless @grapher
|
27
|
-
@grapher = Grapher.new
|
38
|
+
@grapher = Grapher.new(router)
|
28
39
|
@grapher.add_route(self)
|
29
40
|
end
|
30
41
|
@grapher
|
@@ -53,12 +64,17 @@ class Usher
|
|
53
64
|
end
|
54
65
|
|
55
66
|
def find_matching_path(params)
|
56
|
-
if
|
57
|
-
|
67
|
+
#if router.find_matching_paths_based_on_destination_keys?
|
68
|
+
matching_path = if params.nil? || params.empty?
|
69
|
+
@paths.first
|
58
70
|
else
|
59
|
-
|
71
|
+
@paths.size == 1 ? @paths.first : grapher.find_matching_path(params)
|
60
72
|
end
|
61
|
-
|
73
|
+
|
74
|
+
if matching_path.nil? and router.find_matching_paths_based_on_destination_keys?
|
75
|
+
# do something
|
76
|
+
end
|
77
|
+
|
62
78
|
if parent_route
|
63
79
|
matching_path = parent_route.find_matching_path(params).merge(matching_path)
|
64
80
|
matching_path.route = self
|
@@ -67,21 +83,38 @@ class Usher
|
|
67
83
|
matching_path
|
68
84
|
end
|
69
85
|
|
70
|
-
|
86
|
+
CompoundDestination = Struct.new(:args, :block, :options)
|
87
|
+
|
88
|
+
# Sets destination on a route. Returns +self+.
|
89
|
+
#
|
90
|
+
# This method acceps varargs. If you pass in more than one variable, it will be returned to you wrapped in a +CompoundDestination+.
|
91
|
+
# If you send it varargs and the last member is a Hash, it will pop off the hash, and will be stored under <tt>#options</tt>.
|
92
|
+
# Otherwise, if you use send a single variable, or call it with a block, these will be returned to you by <tt>#destination</tt>.
|
71
93
|
#
|
72
94
|
# Request = Struct.new(:path)
|
73
95
|
# set = Usher.new
|
74
96
|
# route = set.add_route('/test')
|
75
97
|
# route.to(:controller => 'testing', :action => 'index')
|
76
98
|
# set.recognize(Request.new('/test')).first.params => {:controller => 'testing', :action => 'index'}
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
99
|
+
#
|
100
|
+
#
|
101
|
+
#
|
102
|
+
def to(*args, &block)
|
103
|
+
if !args.empty? && block
|
104
|
+
@destination = CompoundDestination.new(args, block, args.last.is_a?(Hash) ? args.pop : {})
|
105
|
+
elsif block.nil?
|
106
|
+
case args.size
|
107
|
+
when 0
|
108
|
+
raise "destination should be set as something"
|
109
|
+
when 1
|
110
|
+
@destination = args.first
|
111
|
+
else
|
112
|
+
@destination = CompoundDestination.new(args, nil, args.last.is_a?(Hash) ? args.pop : {})
|
113
|
+
end
|
81
114
|
else
|
82
|
-
|
83
|
-
options
|
115
|
+
@destination = block
|
84
116
|
end
|
117
|
+
args.first.parent_route = self if args.first.respond_to?(:parent_route=)
|
85
118
|
self
|
86
119
|
end
|
87
120
|
|