joshbuddy-usher 0.4.11 → 0.5.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +2 -1
- data/Rakefile +1 -1
- data/VERSION.yml +2 -2
- data/lib/usher/grapher.rb +3 -3
- data/lib/usher/interface/email_interface.rb +1 -1
- data/lib/usher/interface/rails2_2_interface.rb +1 -1
- data/lib/usher/interface/rails2_3_interface.rb +1 -1
- data/lib/usher/node.rb +41 -51
- data/lib/usher/route/path.rb +35 -9
- data/lib/usher/route/util.rb +65 -0
- data/lib/usher/route/variable.rb +22 -15
- data/lib/usher/route.rb +1 -0
- data/lib/usher/splitter.rb +1 -1
- data/lib/usher/util/generate.rb +11 -13
- data/lib/usher/util/parser.rb +25 -78
- data/lib/usher.rb +34 -8
- data/spec/private/email/recognize_spec.rb +2 -4
- data/spec/private/parser_spec.rb +13 -13
- metadata +6 -4
data/README.rdoc
CHANGED
@@ -5,11 +5,12 @@ Tree-based router library. Useful for (specifically) for Rails and Rack, but pro
|
|
5
5
|
== Features
|
6
6
|
|
7
7
|
* Understands single and path-globbing variables
|
8
|
+
* Understands arbitrary regex variables
|
8
9
|
* Arbitrary HTTP header requirements
|
9
10
|
* No optimization phase, so routes are always alterable after the fact
|
10
11
|
* Understands Proc and Regex transformations, validations
|
11
12
|
* Really, really fast
|
12
|
-
* Relatively light and happy code-base, should be easy and fun to alter
|
13
|
+
* Relatively light and happy code-base, should be easy and fun to alter (it hovers around 1,000 LOC, 800 for the core)
|
13
14
|
* Interface and implementation are separate, encouraging cross-pollination
|
14
15
|
|
15
16
|
== Route format
|
data/Rakefile
CHANGED
@@ -9,7 +9,7 @@ begin
|
|
9
9
|
s.homepage = "http://github.com/joshbuddy/usher"
|
10
10
|
s.authors = ["Joshua Hull"]
|
11
11
|
s.files = FileList["[A-Z]*", "{lib,spec,rails}/**/*"]
|
12
|
-
s.add_dependency 'fuzzyhash', '>=0.0.
|
12
|
+
s.add_dependency 'fuzzyhash', '>=0.0.6'
|
13
13
|
end
|
14
14
|
rescue LoadError
|
15
15
|
puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
|
data/VERSION.yml
CHANGED
data/lib/usher/grapher.rb
CHANGED
@@ -14,7 +14,7 @@ class Usher
|
|
14
14
|
|
15
15
|
def add_route(route)
|
16
16
|
route.paths.each do |path|
|
17
|
-
|
17
|
+
if path.dynamic?
|
18
18
|
path.dynamic_keys.each do |k|
|
19
19
|
@orders[path.dynamic_keys.size][k] << path
|
20
20
|
@key_count[k] += 1
|
@@ -50,12 +50,12 @@ class Usher
|
|
50
50
|
end
|
51
51
|
set.size.downto(1) do |o|
|
52
52
|
set.each do |k|
|
53
|
-
@orders[o][k].each
|
53
|
+
@orders[o][k].each do |r|
|
54
54
|
if r.can_generate_from?(set)
|
55
55
|
@cache[set] = r
|
56
56
|
return r
|
57
57
|
end
|
58
|
-
|
58
|
+
end
|
59
59
|
end
|
60
60
|
end
|
61
61
|
nil
|
@@ -3,7 +3,7 @@ class Usher
|
|
3
3
|
class EmailInterface
|
4
4
|
|
5
5
|
def initialize(&blk)
|
6
|
-
@routes = Usher.new(:delimiters => ['@', '-', '.'], :valid_regex => '[\+a-zA-Z0-9]+'
|
6
|
+
@routes = Usher.new(:delimiters => ['@', '-', '.'], :valid_regex => '[\+a-zA-Z0-9]+')
|
7
7
|
instance_eval(&blk) if blk
|
8
8
|
end
|
9
9
|
|
@@ -38,7 +38,7 @@ class Usher
|
|
38
38
|
|
39
39
|
path[0, 0] = '/' unless path[0] == ?/
|
40
40
|
route = @usher.add_route(path, options)
|
41
|
-
raise "your route must include a controller" unless route.paths.first.dynamic_keys.include?(:controller) || route.destination.include?(:controller)
|
41
|
+
raise "your route must include a controller" unless (route.paths.first.dynamic_keys && route.paths.first.dynamic_keys.include?(:controller)) || route.destination.include?(:controller)
|
42
42
|
route
|
43
43
|
end
|
44
44
|
|
@@ -28,7 +28,7 @@ class Usher
|
|
28
28
|
path[0, 0] = '/' unless path[0] == ?/
|
29
29
|
route = @router.add_route(path, options).to(options)
|
30
30
|
|
31
|
-
raise "your route must include a controller" unless (route.paths.first.dynamic_keys.include?(:controller) || route.destination.include?(:controller)
|
31
|
+
raise "your route must include a controller" unless (route.paths.first.dynamic_keys && route.paths.first.dynamic_keys.include?(:controller)) || route.destination.include?(:controller)
|
32
32
|
route
|
33
33
|
end
|
34
34
|
|
data/lib/usher/node.rb
CHANGED
@@ -7,7 +7,7 @@ class Usher
|
|
7
7
|
Response = Struct.new(:path, :params)
|
8
8
|
|
9
9
|
attr_reader :lookup, :greedy_lookup
|
10
|
-
attr_accessor :terminates, :exclusive_type, :parent, :value, :request_methods
|
10
|
+
attr_accessor :terminates, :exclusive_type, :parent, :value, :request_methods
|
11
11
|
|
12
12
|
def initialize(parent, value)
|
13
13
|
@parent = parent
|
@@ -33,10 +33,9 @@ class Usher
|
|
33
33
|
!@greedy_lookup.empty?
|
34
34
|
end
|
35
35
|
|
36
|
-
def self.root(route_set, request_methods
|
36
|
+
def self.root(route_set, request_methods)
|
37
37
|
root = self.new(route_set, nil)
|
38
38
|
root.request_methods = request_methods
|
39
|
-
root.globs_capture_separators = globs_capture_separators
|
40
39
|
root
|
41
40
|
end
|
42
41
|
|
@@ -77,30 +76,25 @@ class Usher
|
|
77
76
|
current_node.lookup[nil] ||= Node.new(current_node, Route::RequestMethod.new(current_node.exclusive_type, nil))
|
78
77
|
end
|
79
78
|
else
|
80
|
-
key.globs_capture_separators = globs_capture_separators if key.is_a?(Route::Variable)
|
81
|
-
|
82
79
|
case key
|
83
80
|
when Route::Variable
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
current_node.greedy_lookup[key.regex_matcher] ||= Node.new(current_node, key)
|
88
|
-
else
|
89
|
-
current_node.greedy_lookup[nil] ||= Node.new(current_node, key)
|
90
|
-
end
|
81
|
+
(upgrade_method, lookup_method) = case key
|
82
|
+
when Route::Variable::Greedy
|
83
|
+
[:upgrade_greedy_lookup, :greedy_lookup]
|
91
84
|
else
|
92
|
-
|
93
|
-
current_node.upgrade_lookup
|
94
|
-
current_node.lookup[key.regex_matcher] ||= Node.new(current_node, key)
|
95
|
-
else
|
96
|
-
current_node.lookup[nil] ||= Node.new(current_node, key)
|
97
|
-
end
|
85
|
+
[:upgrade_lookup, :lookup]
|
98
86
|
end
|
87
|
+
|
88
|
+
if key.regex_matcher
|
89
|
+
current_node.send(upgrade_method)
|
90
|
+
current_node.send(lookup_method)[key.regex_matcher] ||= Node.new(current_node, key)
|
91
|
+
else
|
92
|
+
current_node.send(lookup_method)[nil] ||= Node.new(current_node, key)
|
93
|
+
end
|
99
94
|
else
|
100
95
|
current_node.upgrade_lookup if key.is_a?(Regexp)
|
101
96
|
current_node.lookup[key] ||= Node.new(current_node, key)
|
102
97
|
end
|
103
|
-
|
104
98
|
end
|
105
99
|
current_node = target_node
|
106
100
|
end
|
@@ -118,48 +112,44 @@ class Usher
|
|
118
112
|
end
|
119
113
|
elsif path.size.zero? && terminates?
|
120
114
|
Response.new(terminates, params)
|
121
|
-
elsif !path.size.zero? && (greedy? && (
|
115
|
+
elsif !path.size.zero? && (greedy? && (match_with_result_output = greedy_lookup.match_with_result(whole_path = original_path[position, original_path.size])))
|
116
|
+
next_path, matched_part = match_with_result_output
|
122
117
|
position += matched_part.size
|
123
118
|
params << [next_path.value.name, whole_path.slice!(0, matched_part.size)]
|
124
119
|
next_path.find(usher, request, original_path, whole_path.size.zero? ? whole_path : usher.splitter.url_split(whole_path), params, position)
|
125
120
|
elsif !path.size.zero? && (next_part = lookup[part = path.shift] || lookup[nil])
|
126
121
|
position += part.size
|
127
122
|
case next_part.value
|
128
|
-
when Route::Variable
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
path.unshift(next_part.parent.value)
|
138
|
-
position -= next_part.parent.value.size
|
139
|
-
end
|
140
|
-
break
|
141
|
-
elsif next_part.value.globs_capture_separators
|
142
|
-
params.last.last << part
|
143
|
-
elsif !usher.delimiter_chars.include?(part[0])
|
144
|
-
next_part.value.valid!(part)
|
145
|
-
params.last.last << part
|
146
|
-
end
|
147
|
-
if path.size.zero?
|
148
|
-
break
|
149
|
-
else
|
150
|
-
part = path.shift
|
123
|
+
when Route::Variable::Glob
|
124
|
+
params << [next_part.value.name, []] unless params.last && params.last.first == next_part.value.name
|
125
|
+
loop do
|
126
|
+
if (next_part.value.look_ahead === part || (!usher.delimiter_chars.include?(part[0]) && next_part.value.regex_matcher && !next_part.value.regex_matcher.match(part)))
|
127
|
+
path.unshift(part)
|
128
|
+
position -= part.size
|
129
|
+
if usher.delimiter_chars.include?(next_part.parent.value[0])
|
130
|
+
path.unshift(next_part.parent.value)
|
131
|
+
position -= next_part.parent.value.size
|
151
132
|
end
|
133
|
+
break
|
134
|
+
elsif !usher.delimiter_chars.include?(part[0])
|
135
|
+
next_part.value.valid!(part)
|
136
|
+
params.last.last << part
|
152
137
|
end
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
until (var.look_ahead === path.first) || path.empty?
|
158
|
-
next_path_part = path.shift
|
159
|
-
position += next_path_part.size
|
160
|
-
params.last.last << next_path_part
|
138
|
+
if path.size.zero?
|
139
|
+
break
|
140
|
+
else
|
141
|
+
part = path.shift
|
161
142
|
end
|
162
143
|
end
|
144
|
+
when Route::Variable::Single
|
145
|
+
var = next_part.value
|
146
|
+
var.valid!(part)
|
147
|
+
params << [var.name, part]
|
148
|
+
until (var.look_ahead === path.first) || path.empty?
|
149
|
+
next_path_part = path.shift
|
150
|
+
position += next_path_part.size
|
151
|
+
params.last.last << next_path_part
|
152
|
+
end
|
163
153
|
end
|
164
154
|
next_part.find(usher, request, original_path, path, params, position)
|
165
155
|
else
|
data/lib/usher/route/path.rb
CHANGED
@@ -2,22 +2,48 @@ class Usher
|
|
2
2
|
class Route
|
3
3
|
class Path
|
4
4
|
|
5
|
-
attr_reader :
|
5
|
+
attr_reader :route, :parts
|
6
6
|
|
7
7
|
def initialize(route, parts)
|
8
8
|
@route = route
|
9
9
|
@parts = parts
|
10
|
-
@
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
10
|
+
@dynamic = @parts.any?{|p| p.is_a?(Variable)}
|
11
|
+
end
|
12
|
+
|
13
|
+
def dynamic_indicies
|
14
|
+
unless @dynamic_indicies
|
15
|
+
@dynamic_indicies = []
|
16
|
+
parts.each_index{|i| @dynamic_indicies << i if parts[i].is_a?(Variable)}
|
17
|
+
end
|
18
|
+
@dynamic_indicies
|
19
|
+
end
|
20
|
+
|
21
|
+
def dynamic_parts
|
22
|
+
@dynamic_parts ||= parts.values_at(*dynamic_indicies)
|
23
|
+
end
|
24
|
+
|
25
|
+
def dynamic_map
|
26
|
+
unless @dynamic_map
|
27
|
+
@dynamic_map = {}
|
28
|
+
dynamic_parts.each{|p| @dynamic_map[p.name] = p }
|
29
|
+
end
|
30
|
+
@dynamic_map
|
31
|
+
end
|
32
|
+
|
33
|
+
def dynamic_keys
|
34
|
+
@dynamic_keys ||= dynamic_map.keys
|
35
|
+
end
|
36
|
+
|
37
|
+
def dynamic_required_keys
|
38
|
+
@dynamic_required_keys ||= dynamic_parts.select{|dp| !dp.default_value}.map{|dp| dp.name}
|
39
|
+
end
|
40
|
+
|
41
|
+
def dynamic?
|
42
|
+
@dynamic
|
17
43
|
end
|
18
44
|
|
19
45
|
def can_generate_from?(keys)
|
20
|
-
(
|
46
|
+
(dynamic_required_keys - keys).size.zero?
|
21
47
|
end
|
22
48
|
end
|
23
49
|
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
class Usher
|
2
|
+
class Route
|
3
|
+
|
4
|
+
module Util
|
5
|
+
|
6
|
+
class Group < Array
|
7
|
+
attr_accessor :group_type
|
8
|
+
attr_accessor :parent
|
9
|
+
|
10
|
+
def inspect
|
11
|
+
"#{group_type}->#{super}"
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(group_type, parent)
|
15
|
+
@group_type = group_type
|
16
|
+
@parent = parent
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.cartesian_product!(lval, rval)
|
21
|
+
product = []
|
22
|
+
(lval.size * rval.size).times do |index|
|
23
|
+
val = []
|
24
|
+
val.push(*lval[index % lval.size])
|
25
|
+
val.push(*rval[index % rval.size])
|
26
|
+
product << val
|
27
|
+
end
|
28
|
+
lval.replace(product)
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.expand_path(parts)
|
32
|
+
if parts.is_a?(Array)
|
33
|
+
paths = [[]]
|
34
|
+
|
35
|
+
unless parts.respond_to?(:group_type)
|
36
|
+
new_parts = Group.new(:any, nil)
|
37
|
+
parts.each{|p| new_parts << p}
|
38
|
+
parts = new_parts
|
39
|
+
end
|
40
|
+
|
41
|
+
case parts.group_type
|
42
|
+
when :all
|
43
|
+
parts.each do |p|
|
44
|
+
cartesian_product!(paths, expand_path(p))
|
45
|
+
end
|
46
|
+
when :any
|
47
|
+
parts.each do |p|
|
48
|
+
cartesian_product!(paths, expand_path(p))
|
49
|
+
end
|
50
|
+
paths.unshift([])
|
51
|
+
when :one
|
52
|
+
cartesian_product!(paths, parts.collect do |p|
|
53
|
+
expand_path(p)
|
54
|
+
end)
|
55
|
+
end
|
56
|
+
paths.each{|p| p.compact!; p.flatten! }
|
57
|
+
paths
|
58
|
+
else
|
59
|
+
[[parts]]
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
data/lib/usher/route/variable.rb
CHANGED
@@ -2,23 +2,14 @@ class Usher
|
|
2
2
|
class Route
|
3
3
|
class Variable
|
4
4
|
attr_reader :type, :name, :validator, :regex_matcher
|
5
|
-
attr_accessor :look_ahead, :
|
5
|
+
attr_accessor :look_ahead, :default_value
|
6
6
|
|
7
|
-
def initialize(
|
8
|
-
@
|
9
|
-
@name = :"#{name}"
|
7
|
+
def initialize(name, regex_matcher = nil, validator = nil)
|
8
|
+
@name = name.to_s.to_sym
|
10
9
|
@validator = validator
|
11
10
|
@regex_matcher = regex_matcher
|
12
|
-
@globs_capture_separators = globs_capture_separators
|
13
|
-
end
|
14
|
-
|
15
|
-
def to_s
|
16
|
-
"#{type}#{name}"
|
17
|
-
end
|
18
|
-
|
19
|
-
def greedy?
|
20
|
-
type == :'!'
|
21
11
|
end
|
12
|
+
private :initialize
|
22
13
|
|
23
14
|
def valid!(val)
|
24
15
|
case @validator
|
@@ -26,7 +17,7 @@ class Usher
|
|
26
17
|
begin
|
27
18
|
@validator.call(val)
|
28
19
|
rescue Exception => e
|
29
|
-
raise ValidationException.new("#{val} does not conform to #{@
|
20
|
+
raise ValidationException.new("#{val} does not conform to #{@g}, root cause #{e.inspect}")
|
30
21
|
end
|
31
22
|
else
|
32
23
|
@validator === val or raise(ValidationException.new("#{val} does not conform to #{@validator}, root cause #{e.inspect}"))
|
@@ -34,8 +25,24 @@ class Usher
|
|
34
25
|
end
|
35
26
|
|
36
27
|
def ==(o)
|
37
|
-
o && (o.
|
28
|
+
o && (o.class == self.class && o.name == @name && o.validator == @validator)
|
29
|
+
end
|
30
|
+
|
31
|
+
class Single < Variable
|
32
|
+
def to_s
|
33
|
+
":#{name}"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
class Glob < Variable
|
38
|
+
def to_s
|
39
|
+
"*#{name}"
|
40
|
+
end
|
38
41
|
end
|
42
|
+
|
43
|
+
Greedy = Class.new(Variable)
|
44
|
+
|
39
45
|
end
|
46
|
+
|
40
47
|
end
|
41
48
|
end
|
data/lib/usher/route.rb
CHANGED
data/lib/usher/splitter.rb
CHANGED
@@ -4,7 +4,7 @@ class Usher
|
|
4
4
|
class Splitter
|
5
5
|
|
6
6
|
def self.for_delimiters(router, valid_regex)
|
7
|
-
SplitterInstance.new(Regexp.new("[#{router.delimiters.collect{|d| Regexp.quote(d)}}]|[^#{router.delimiters.collect{|d| Regexp.quote(d)}}]+"))
|
7
|
+
SplitterInstance.new(Regexp.new("[#{router.delimiters.collect{|d| Regexp.quote(d)}.join}]|[^#{router.delimiters.collect{|d| Regexp.quote(d)}.join}]+"))
|
8
8
|
end
|
9
9
|
|
10
10
|
class SplitterInstance
|
data/lib/usher/util/generate.rb
CHANGED
@@ -91,21 +91,19 @@ class Usher
|
|
91
91
|
result = ''
|
92
92
|
path.parts.each do |part|
|
93
93
|
case part
|
94
|
-
when Route::Variable
|
94
|
+
when Route::Variable::Glob
|
95
95
|
value = (params && params.delete(part.name)) || part.default_value || raise(MissingParameterException.new)
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
result << current_value
|
102
|
-
result << '/' if index != value.size - 1
|
103
|
-
end
|
104
|
-
when :':'
|
105
|
-
value = value.to_s unless value.is_a?(String)
|
106
|
-
part.valid!(value)
|
107
|
-
result << value
|
96
|
+
value.each_with_index do |current_value, index|
|
97
|
+
current_value = current_value.to_s unless current_value.is_a?(String)
|
98
|
+
part.valid!(current_value)
|
99
|
+
result << current_value
|
100
|
+
result << '/' if index != value.size - 1
|
108
101
|
end
|
102
|
+
when Route::Variable
|
103
|
+
value = (params && params.delete(part.name)) || part.default_value || raise(MissingParameterException.new)
|
104
|
+
value = value.to_s unless value.is_a?(String)
|
105
|
+
part.valid!(value)
|
106
|
+
result << value
|
109
107
|
else
|
110
108
|
result << part
|
111
109
|
end
|
data/lib/usher/util/parser.rb
CHANGED
@@ -18,16 +18,23 @@ class Usher
|
|
18
18
|
@split_regex = split_regex
|
19
19
|
end
|
20
20
|
|
21
|
+
def parse_and_expand(path, requirements = nil, default_values = nil)
|
22
|
+
Usher::Route::Util.expand_path(parse(path, requirements, default_values))
|
23
|
+
end
|
24
|
+
|
21
25
|
def parse(path, requirements = nil, default_values = nil)
|
22
|
-
parts = Group.new(:all, nil)
|
26
|
+
parts = Usher::Route::Util::Group.new(:all, nil)
|
23
27
|
ss = StringScanner.new(path)
|
24
28
|
current_group = parts
|
25
29
|
while !ss.eos?
|
26
30
|
part = ss.scan(@split_regex)
|
27
31
|
case part[0]
|
28
|
-
when
|
29
|
-
|
30
|
-
current_group << Usher::Route::Variable.new(
|
32
|
+
when ?*
|
33
|
+
var_name = part[1, part.size - 1].to_sym
|
34
|
+
current_group << Usher::Route::Variable::Glob.new(part[1, part.size - 1], nil, requirements && requirements[var_name])
|
35
|
+
when ?:
|
36
|
+
var_name = part[1, part.size - 1].to_sym
|
37
|
+
current_group << Usher::Route::Variable::Single.new(part[1, part.size - 1], nil, requirements && requirements[var_name])
|
31
38
|
when ?{
|
32
39
|
pattern = ''
|
33
40
|
count = 1
|
@@ -46,103 +53,43 @@ class Usher
|
|
46
53
|
regex = Regexp.new(pattern)
|
47
54
|
if variable
|
48
55
|
variable_type = variable.slice!(0).chr.to_sym
|
56
|
+
variable_class = case variable_type
|
57
|
+
when :'!' then Usher::Route::Variable::Greedy
|
58
|
+
when :* then Usher::Route::Variable::Glob
|
59
|
+
when :':' then Usher::Route::Variable::Single
|
60
|
+
end
|
61
|
+
|
49
62
|
variable_name = variable[0, variable.size - 1].to_sym
|
50
|
-
current_group <<
|
63
|
+
current_group << variable_class.new(variable_name, regex, requirements && requirements[variable_name])
|
51
64
|
else
|
52
65
|
current_group << regex
|
53
66
|
end
|
54
67
|
when ?(
|
55
|
-
new_group = Group.new(:any, current_group)
|
68
|
+
new_group = Usher::Route::Util::Group.new(:any, current_group)
|
56
69
|
current_group << new_group
|
57
70
|
current_group = new_group
|
58
71
|
when ?)
|
59
|
-
current_group = current_group.parent.
|
72
|
+
current_group = current_group.parent.group_type == :one ? current_group.parent.parent : current_group.parent
|
60
73
|
when ?|
|
61
|
-
unless current_group.parent.
|
74
|
+
unless current_group.parent.group_type == :one
|
62
75
|
detached_group = current_group.parent.pop
|
63
|
-
new_group = Group.new(:one, detached_group.parent)
|
76
|
+
new_group = Usher::Route::Util::Group.new(:one, detached_group.parent)
|
64
77
|
detached_group.parent = new_group
|
65
|
-
detached_group.
|
78
|
+
detached_group.group_type = :all
|
66
79
|
new_group << detached_group
|
67
80
|
new_group.parent << new_group
|
68
81
|
end
|
69
|
-
current_group.parent << Group.new(:all, current_group.parent)
|
82
|
+
current_group.parent << Usher::Route::Util::Group.new(:all, current_group.parent)
|
70
83
|
current_group = current_group.parent.last
|
71
84
|
else
|
72
85
|
current_group << part
|
73
86
|
end
|
74
87
|
end unless !path || path.empty?
|
75
|
-
|
76
|
-
paths.each do |path|
|
77
|
-
path.each_with_index do |part, index|
|
78
|
-
if part.is_a?(Usher::Route::Variable)
|
79
|
-
part.default_value = default_values[part.name] if default_values
|
80
|
-
|
81
|
-
case part.type
|
82
|
-
when :*
|
83
|
-
part.look_ahead = path[index + 1, path.size].find{|p| !p.is_a?(Usher::Route::Variable) && !@router.delimiter_chars.include?(p[0])} || nil
|
84
|
-
when :':'
|
85
|
-
part.look_ahead = path[index + 1, path.size].find{|p| @router.delimiter_chars.include?(p[0])} || @router.delimiters.first
|
86
|
-
end
|
87
|
-
end
|
88
|
-
end
|
89
|
-
end
|
90
|
-
paths
|
91
|
-
end
|
92
|
-
|
93
|
-
private
|
94
|
-
|
95
|
-
def cartesian_product!(lval, rval)
|
96
|
-
product = []
|
97
|
-
(lval.size * rval.size).times do |index|
|
98
|
-
val = []
|
99
|
-
val.push(*lval[index % lval.size])
|
100
|
-
val.push(*rval[index % rval.size])
|
101
|
-
product << val
|
102
|
-
end
|
103
|
-
lval.replace(product)
|
88
|
+
parts
|
104
89
|
end
|
105
90
|
|
106
|
-
def calc_paths(parts)
|
107
|
-
if parts.is_a?(Group)
|
108
|
-
paths = [[]]
|
109
|
-
case parts.type
|
110
|
-
when :all
|
111
|
-
parts.each do |p|
|
112
|
-
cartesian_product!(paths, calc_paths(p))
|
113
|
-
end
|
114
|
-
when :any
|
115
|
-
parts.each do |p|
|
116
|
-
cartesian_product!(paths, calc_paths(p))
|
117
|
-
end
|
118
|
-
paths.unshift([])
|
119
|
-
when :one
|
120
|
-
cartesian_product!(paths, parts.collect do |p|
|
121
|
-
calc_paths(p)
|
122
|
-
end)
|
123
|
-
end
|
124
|
-
paths.each{|p| p.compact!; p.flatten! }
|
125
|
-
paths
|
126
|
-
else
|
127
|
-
[[parts]]
|
128
|
-
end
|
129
|
-
|
130
|
-
end
|
131
91
|
end
|
132
92
|
|
133
|
-
class Group < Array
|
134
|
-
attr_accessor :type
|
135
|
-
attr_accessor :parent
|
136
|
-
|
137
|
-
def inspect
|
138
|
-
"#{type}->#{super}"
|
139
|
-
end
|
140
|
-
|
141
|
-
def initialize(type, parent)
|
142
|
-
@type = type
|
143
|
-
@parent = parent
|
144
|
-
end
|
145
|
-
end
|
146
93
|
end
|
147
94
|
end
|
148
95
|
end
|
data/lib/usher.rb
CHANGED
@@ -31,7 +31,7 @@ class Usher
|
|
31
31
|
# set.reset!
|
32
32
|
# set.empty? => true
|
33
33
|
def reset!
|
34
|
-
@tree = Node.root(self, @request_methods
|
34
|
+
@tree = Node.root(self, @request_methods)
|
35
35
|
@named_routes = {}
|
36
36
|
@routes = []
|
37
37
|
@route_count = 0
|
@@ -41,9 +41,6 @@ class Usher
|
|
41
41
|
|
42
42
|
# Creates a route set, with options
|
43
43
|
#
|
44
|
-
# <tt>:globs_capture_separators</tt>: +true+ or +false+. (default +false+) Specifies whether glob matching will also include separators
|
45
|
-
# that are matched.
|
46
|
-
#
|
47
44
|
# <tt>:delimiters</tt>: Array of Strings. (default <tt>['/', '.']</tt>). Delimiters used in path separation. Array must be single character strings.
|
48
45
|
#
|
49
46
|
# <tt>:valid_regex</tt>: String. (default <tt>'[0-9A-Za-z\$\-_\+!\*\',]+'</tt>). String that can be interpolated into regex to match
|
@@ -52,7 +49,6 @@ class Usher
|
|
52
49
|
# <tt>:request_methods</tt>: Array of Symbols. (default <tt>[:protocol, :domain, :port, :query_string, :remote_ip, :user_agent, :referer, :method, :subdomains]</tt>)
|
53
50
|
# Array of methods called against the request object for the purposes of matching route requirements.
|
54
51
|
def initialize(options = nil)
|
55
|
-
@globs_capture_separators = options && options.key?(:globs_capture_separators) ? options.delete(:globs_capture_separators) : false
|
56
52
|
@delimiters = options && options.delete(:delimiters) || ['/', '.']
|
57
53
|
@delimiter_chars = @delimiters.collect{|d| d[0]}
|
58
54
|
@delimiters_regex = @delimiters.collect{|d| Regexp.quote(d)} * '|'
|
@@ -154,7 +150,7 @@ class Usher
|
|
154
150
|
# * +requirements+ - After transformation, tests the condition using ===. If it returns false, it raises an <tt>Usher::ValidationException</tt>
|
155
151
|
# * +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.
|
156
152
|
# * Any other key is interpreted as a requirement for the variable of its name.
|
157
|
-
def add_route(
|
153
|
+
def add_route(unprocessed_path, options = nil)
|
158
154
|
conditions = options && options.delete(:conditions) || nil
|
159
155
|
requirements = options && options.delete(:requirements) || nil
|
160
156
|
default_values = options && options.delete(:default_values) || nil
|
@@ -168,10 +164,30 @@ class Usher
|
|
168
164
|
end
|
169
165
|
end
|
170
166
|
|
171
|
-
|
167
|
+
unprocessed_path = parser.parse(unprocessed_path, requirements, default_values) if unprocessed_path.is_a?(String)
|
168
|
+
|
169
|
+
unless unprocessed_path.first.is_a?(Route::Util::Group)
|
170
|
+
group = Usher::Route::Util::Group.new(:all, nil)
|
171
|
+
unprocessed_path.each{|p| group << p}
|
172
|
+
unprocessed_path = group
|
173
|
+
end
|
174
|
+
|
175
|
+
paths = Route::Util.expand_path(unprocessed_path)
|
172
176
|
|
177
|
+
paths.each do |path|
|
178
|
+
path.each_with_index do |part, index|
|
179
|
+
part.default_value = default_values[part.name] if part.is_a?(Usher::Route::Variable) && default_values && default_values[part.name]
|
180
|
+
case part
|
181
|
+
when Usher::Route::Variable::Glob
|
182
|
+
part.look_ahead = path[index + 1, path.size].find{|p| !p.is_a?(Usher::Route::Variable) && !delimiter_chars.include?(p[0])} || nil
|
183
|
+
when Usher::Route::Variable
|
184
|
+
part.look_ahead = path[index + 1, path.size].find{|p| delimiter_chars.include?(p[0])} || delimiters.first
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
173
189
|
route = Route.new(
|
174
|
-
|
190
|
+
paths,
|
175
191
|
self,
|
176
192
|
conditions,
|
177
193
|
requirements,
|
@@ -198,6 +214,16 @@ class Usher
|
|
198
214
|
@tree.find(self, request, path, @splitter.url_split(path))
|
199
215
|
end
|
200
216
|
|
217
|
+
# 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.
|
218
|
+
#
|
219
|
+
# Request = Struct.new(:path)
|
220
|
+
# set = Usher.new
|
221
|
+
# route = set.add_route('/test')
|
222
|
+
# set.recognize_path('/test').path.route == route => true
|
223
|
+
def recognize_path(path)
|
224
|
+
recognize(nil, path)
|
225
|
+
end
|
226
|
+
|
201
227
|
# Recognizes a set of +parameters+ and gets the closest matching Usher::Route::Path or +nil+ if no route exists.
|
202
228
|
#
|
203
229
|
# set = Usher.new
|
@@ -1,7 +1,5 @@
|
|
1
1
|
require 'lib/usher'
|
2
2
|
|
3
|
-
|
4
|
-
|
5
3
|
def build_email_mock(email)
|
6
4
|
request = mock "Request"
|
7
5
|
request.should_receive(:email).any_number_of_times.and_return(email)
|
@@ -24,14 +22,14 @@ describe "Usher (for email) route recognition" do
|
|
24
22
|
it "should recognize a wildcard domain" do
|
25
23
|
receiver = mock('receiver')
|
26
24
|
receiver.should_receive(:action).with({:domain => 'gmail.com'}).exactly(1)
|
27
|
-
@route_set.for('joshbuddy
|
25
|
+
@route_set.for('joshbuddy@{!domain,.*}') { |params| receiver.action(params) }
|
28
26
|
@route_set.act('joshbuddy@gmail.com')
|
29
27
|
end
|
30
28
|
|
31
29
|
it "should recognize a complex email" do
|
32
30
|
receiver = mock('receiver')
|
33
31
|
receiver.should_receive(:action).with({:subject => 'sub+ect', :id => '123', :sid => '456', :tok => 'sdqwe123ae', :domain => 'mydomain.org'}).exactly(1)
|
34
|
-
@route_set.for(':subject.{
|
32
|
+
@route_set.for(':subject.{!id,\d+}-{!sid,\d+}-{!tok,\w+}@{!domain,.*}') { |params| receiver.action(params) }
|
35
33
|
@route_set.act('sub+ect.123-456-sdqwe123ae@mydomain.org')
|
36
34
|
end
|
37
35
|
|
data/spec/private/parser_spec.rb
CHANGED
@@ -4,41 +4,41 @@ describe "Usher route tokenizing" do
|
|
4
4
|
|
5
5
|
|
6
6
|
it "should split / delimited routes" do
|
7
|
-
Usher.new(:delimiters => ['/', '.'], :valid_regex => '[0-9A-Za-z\$\-_\+!\*\',]+').parser.
|
7
|
+
Usher.new(:delimiters => ['/', '.'], :valid_regex => '[0-9A-Za-z\$\-_\+!\*\',]+').parser.parse_and_expand('/test/this/split').should == [['/', 'test', '/','this', '/', 'split']]
|
8
8
|
end
|
9
9
|
|
10
10
|
it "should split / delimited routes with a regex in it" do
|
11
|
-
Usher.new(:delimiters => ['/', '.'], :valid_regex => '[0-9A-Za-z\$\-_\+!\*\',]+').parser.
|
11
|
+
Usher.new(:delimiters => ['/', '.'], :valid_regex => '[0-9A-Za-z\$\-_\+!\*\',]+').parser.parse_and_expand('/test/{this}/split').should == [['/', 'test', '/', /this/, '/', 'split']]
|
12
12
|
end
|
13
13
|
|
14
14
|
it "should split on ' ' delimited routes as well" do
|
15
|
-
Usher.new(:delimiters => [' '], :valid_regex => '[0-9A-Za-z\$\-_\+!\*\',]+').parser.
|
15
|
+
Usher.new(:delimiters => [' '], :valid_regex => '[0-9A-Za-z\$\-_\+!\*\',]+').parser.parse_and_expand('test this split').should == [['test', ' ', 'this', ' ', 'split']]
|
16
16
|
end
|
17
17
|
|
18
18
|
it "should split on email delimiters as well" do
|
19
|
-
Usher.new(:delimiters => ['@', '+', '-', '.'], :valid_regex => '[a-zA-Z0-9]+').parser.
|
19
|
+
Usher.new(:delimiters => ['@', '+', '-', '.'], :valid_regex => '[a-zA-Z0-9]+').parser.parse_and_expand('one+more.12345-09876-alphanum3ric5@domain.com').should == [["one", '+', "more", ".", "12345", '-', "09876", '-', "alphanum3ric5", "@", "domain", ".", "com"]]
|
20
20
|
end
|
21
21
|
|
22
22
|
it "should split on ' ' delimited routes for more complex routes as well" do
|
23
|
-
Usher.new(:delimiters => [' '], :valid_regex => '[0-9A-Za-z\$\-_\+!\*\',]+').parser.
|
23
|
+
Usher.new(:delimiters => [' '], :valid_regex => '[0-9A-Za-z\$\-_\+!\*\',]+').parser.parse_and_expand('(test|this) split').should == [['test', ' ', 'split'], ['this', ' ', 'split']]
|
24
24
|
end
|
25
25
|
|
26
26
|
it "should group optional parts with brackets" do
|
27
|
-
Usher.new(:delimiters => ['/', '.'], :valid_regex => '[0-9A-Za-z\$\-_\+!\*\',]+').parser.
|
27
|
+
Usher.new(:delimiters => ['/', '.'], :valid_regex => '[0-9A-Za-z\$\-_\+!\*\',]+').parser.parse_and_expand('/test/this(/split)').should == [
|
28
28
|
['/', 'test', '/', 'this'],
|
29
29
|
['/', 'test', '/', 'this', '/', 'split']
|
30
30
|
]
|
31
31
|
end
|
32
32
|
|
33
33
|
it "should group exclusive optional parts with brackets and pipes" do
|
34
|
-
Usher.new(:delimiters => ['/', '.'], :valid_regex => '[0-9A-Za-z\$\-_\+!\*\',]+').parser.
|
34
|
+
Usher.new(:delimiters => ['/', '.'], :valid_regex => '[0-9A-Za-z\$\-_\+!\*\',]+').parser.parse_and_expand('/test/this(/split|/split2)').should == [
|
35
35
|
['/', 'test', '/', 'this','/', 'split'],
|
36
36
|
['/', 'test', '/', 'this','/', 'split2']
|
37
37
|
]
|
38
38
|
end
|
39
39
|
|
40
40
|
it "should group exclusive optional-optional parts with brackets and pipes" do
|
41
|
-
Usher.new(:delimiters => ['/', '.'], :valid_regex => '[0-9A-Za-z\$\-_\+!\*\',]+').parser.
|
41
|
+
Usher.new(:delimiters => ['/', '.'], :valid_regex => '[0-9A-Za-z\$\-_\+!\*\',]+').parser.parse_and_expand('/test/this((/split|/split2))').should == [
|
42
42
|
['/', 'test','/', 'this'],
|
43
43
|
['/', 'test','/', 'this', '/', 'split'],
|
44
44
|
['/', 'test','/', 'this', '/', 'split2']
|
@@ -46,7 +46,7 @@ describe "Usher route tokenizing" do
|
|
46
46
|
end
|
47
47
|
|
48
48
|
it "should group optional parts with brackets (for non overlapping groups)" do
|
49
|
-
Usher.new(:delimiters => ['/', '.'], :valid_regex => '[0-9A-Za-z\$\-_\+!\*\',]+').parser.
|
49
|
+
Usher.new(:delimiters => ['/', '.'], :valid_regex => '[0-9A-Za-z\$\-_\+!\*\',]+').parser.parse_and_expand('/test/this(/split)(/split2)') == [
|
50
50
|
['/', "test", '/', "this"],
|
51
51
|
['/', "test", '/', "this", '/', "split"],
|
52
52
|
['/', "test", '/', "this", '/', "split2"],
|
@@ -55,20 +55,20 @@ describe "Usher route tokenizing" do
|
|
55
55
|
end
|
56
56
|
|
57
57
|
it "should group nested-optional parts with brackets" do
|
58
|
-
Usher.new(:delimiters => ['/', '.'], :valid_regex => '[0-9A-Za-z\$\-_\+!\*\',]+').parser.
|
58
|
+
Usher.new(:delimiters => ['/', '.'], :valid_regex => '[0-9A-Za-z\$\-_\+!\*\',]+').parser.parse_and_expand('/test/this(/split(.:format))') == [
|
59
59
|
['/', "test", '/', "this"],
|
60
60
|
['/', "test", '/', "this", '/', "split"],
|
61
|
-
['/', "test", '/', "this", '/', "split", '.', Usher::Route::Variable.new(:
|
61
|
+
['/', "test", '/', "this", '/', "split", '.', Usher::Route::Variable::Single.new(:format)]
|
62
62
|
]
|
63
63
|
end
|
64
64
|
|
65
65
|
it "should to_s all different variable types" do
|
66
|
-
Usher.new(:delimiters => ['/', '.'], :valid_regex => '[0-9A-Za-z\$\-_\+!\*\',]+').parser.
|
66
|
+
Usher.new(:delimiters => ['/', '.'], :valid_regex => '[0-9A-Za-z\$\-_\+!\*\',]+').parser.parse_and_expand('/:split/*splitter').first.collect{|v| v.to_s} ==
|
67
67
|
[ ':split', '*splitter' ]
|
68
68
|
end
|
69
69
|
|
70
70
|
it "should == variable types" do
|
71
|
-
parts = Usher.new(:delimiters => ['/', '.'], :valid_regex => '[0-9A-Za-z\$\-_\+!\*\',]+').parser.
|
71
|
+
parts = Usher.new(:delimiters => ['/', '.'], :valid_regex => '[0-9A-Za-z\$\-_\+!\*\',]+').parser.parse_and_expand('/:split/:split').first
|
72
72
|
parts[1].should == parts[3]
|
73
73
|
end
|
74
74
|
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: joshbuddy-usher
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.5.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Joshua Hull
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2009-
|
12
|
+
date: 2009-08-22 00:00:00 -07:00
|
13
13
|
default_executable:
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
@@ -20,7 +20,7 @@ dependencies:
|
|
20
20
|
requirements:
|
21
21
|
- - ">="
|
22
22
|
- !ruby/object:Gem::Version
|
23
|
-
version: 0.0.
|
23
|
+
version: 0.0.6
|
24
24
|
version:
|
25
25
|
description: A general purpose routing library
|
26
26
|
email: joshbuddy@gmail.com
|
@@ -52,6 +52,7 @@ files:
|
|
52
52
|
- lib/usher/route.rb
|
53
53
|
- lib/usher/route/path.rb
|
54
54
|
- lib/usher/route/request_method.rb
|
55
|
+
- lib/usher/route/util.rb
|
55
56
|
- lib/usher/route/variable.rb
|
56
57
|
- lib/usher/splitter.rb
|
57
58
|
- lib/usher/util.rb
|
@@ -77,6 +78,7 @@ files:
|
|
77
78
|
- spec/spec.opts
|
78
79
|
has_rdoc: false
|
79
80
|
homepage: http://github.com/joshbuddy/usher
|
81
|
+
licenses:
|
80
82
|
post_install_message:
|
81
83
|
rdoc_options:
|
82
84
|
- --charset=UTF-8
|
@@ -97,7 +99,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
97
99
|
requirements: []
|
98
100
|
|
99
101
|
rubyforge_project:
|
100
|
-
rubygems_version: 1.
|
102
|
+
rubygems_version: 1.3.5
|
101
103
|
signing_key:
|
102
104
|
specification_version: 3
|
103
105
|
summary: A general purpose routing library
|