joshbuddy-usher 0.4.11 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|