usher 0.4.8 → 0.5.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +12 -1
- data/Rakefile +5 -29
- data/VERSION.yml +2 -2
- data/lib/usher.rb +126 -37
- data/lib/usher/grapher.rb +5 -4
- data/lib/usher/interface.rb +12 -5
- data/lib/usher/interface/email_interface.rb +1 -1
- data/lib/usher/interface/rack_interface.rb +31 -13
- data/lib/usher/interface/rails2_2_interface.rb +3 -4
- data/lib/usher/interface/rails2_3_interface.rb +3 -4
- data/lib/usher/interface/rails3_interface.rb +57 -0
- data/lib/usher/node.rb +121 -73
- data/lib/usher/route.rb +45 -11
- data/lib/usher/route/path.rb +50 -11
- data/lib/usher/route/util.rb +65 -0
- data/lib/usher/route/variable.rb +22 -11
- data/lib/usher/splitter.rb +4 -141
- data/lib/usher/util.rb +6 -0
- data/lib/usher/util/generate.rb +129 -0
- data/lib/usher/util/parser.rb +145 -0
- data/spec/private/email/recognize_spec.rb +2 -4
- data/spec/private/generate_spec.rb +86 -32
- data/spec/private/grapher_spec.rb +5 -6
- data/spec/private/parser_spec.rb +75 -0
- data/spec/private/path_spec.rb +35 -4
- data/spec/private/rack/dispatch_spec.rb +100 -15
- data/spec/private/recognize_spec.rb +88 -50
- data/spec/spec_helper.rb +22 -0
- metadata +13 -7
- data/lib/usher/generate.rb +0 -131
- data/spec/private/split_spec.rb +0 -76
data/lib/usher/route/path.rb
CHANGED
@@ -2,23 +2,62 @@ class Usher
|
|
2
2
|
class Route
|
3
3
|
class Path
|
4
4
|
|
5
|
-
|
5
|
+
attr_accessor :route
|
6
|
+
attr_reader :parts
|
6
7
|
|
7
8
|
def initialize(route, parts)
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
9
|
+
self.route = route
|
10
|
+
self.parts = parts
|
11
|
+
end
|
12
|
+
|
13
|
+
def dynamic_indicies
|
14
|
+
unless dynamic? && @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) if dynamic?
|
23
|
+
end
|
24
|
+
|
25
|
+
def dynamic_map
|
26
|
+
unless dynamic? && @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 if dynamic?
|
35
|
+
end
|
36
|
+
|
37
|
+
def dynamic_required_keys
|
38
|
+
@dynamic_required_keys ||= dynamic_parts.select{|dp| !dp.default_value}.map{|dp| dp.name} if dynamic?
|
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?
|
47
|
+
end
|
48
|
+
|
49
|
+
# Merges paths for use in generation
|
50
|
+
def merge(other_path)
|
51
|
+
new_parts = parts + other_path.parts
|
52
|
+
Path.new(route, new_parts)
|
21
53
|
end
|
54
|
+
|
55
|
+
private
|
56
|
+
def parts=(parts)
|
57
|
+
@parts = parts
|
58
|
+
@dynamic = @parts.any?{|p| p.is_a?(Variable)}
|
59
|
+
end
|
60
|
+
|
22
61
|
end
|
23
62
|
end
|
24
63
|
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,19 +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
11
|
end
|
12
|
+
private :initialize
|
18
13
|
|
19
14
|
def valid!(val)
|
20
15
|
case @validator
|
@@ -22,7 +17,7 @@ class Usher
|
|
22
17
|
begin
|
23
18
|
@validator.call(val)
|
24
19
|
rescue Exception => e
|
25
|
-
raise ValidationException.new("#{val} does not conform to #{@
|
20
|
+
raise ValidationException.new("#{val} does not conform to #{@g}, root cause #{e.inspect}")
|
26
21
|
end
|
27
22
|
else
|
28
23
|
@validator === val or raise(ValidationException.new("#{val} does not conform to #{@validator}, root cause #{e.inspect}"))
|
@@ -30,8 +25,24 @@ class Usher
|
|
30
25
|
end
|
31
26
|
|
32
27
|
def ==(o)
|
33
|
-
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
|
34
35
|
end
|
36
|
+
|
37
|
+
class Glob < Variable
|
38
|
+
def to_s
|
39
|
+
"*#{name}"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
Greedy = Class.new(Variable)
|
44
|
+
|
35
45
|
end
|
46
|
+
|
36
47
|
end
|
37
48
|
end
|
data/lib/usher/splitter.rb
CHANGED
@@ -3,156 +3,19 @@ require 'strscan'
|
|
3
3
|
class Usher
|
4
4
|
class Splitter
|
5
5
|
|
6
|
-
def self.for_delimiters(
|
7
|
-
|
8
|
-
SplitterInstance.new(
|
9
|
-
delimiters,
|
10
|
-
Regexp.new('((:|\*)?' + valid_regex + '|' + delimiters_regex + '|\(|\)|\||\{)'),
|
11
|
-
Regexp.new("[#{delimiters.collect{|d| Regexp.quote(d)}}]|[^#{delimiters.collect{|d| Regexp.quote(d)}}]+")
|
12
|
-
)
|
6
|
+
def self.for_delimiters(router, valid_regex)
|
7
|
+
SplitterInstance.new(Regexp.new("[#{router.delimiters.collect{|d| Regexp.quote(d)}.join}]|[^#{router.delimiters.collect{|d| Regexp.quote(d)}.join}]+"))
|
13
8
|
end
|
14
|
-
|
15
|
-
attr_reader :paths
|
16
9
|
|
17
10
|
class SplitterInstance
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
def initialize(delimiters, split_regex, url_split_regex)
|
22
|
-
@delimiters = delimiters
|
23
|
-
@delimiter_chars = delimiters.collect{|d| d[0]}
|
24
|
-
@split_regex = split_regex
|
11
|
+
|
12
|
+
def initialize(url_split_regex)
|
25
13
|
@url_split_regex = url_split_regex
|
26
14
|
end
|
27
15
|
|
28
16
|
def url_split(path)
|
29
17
|
path.scan(@url_split_regex)
|
30
18
|
end
|
31
|
-
|
32
|
-
def split(path, requirements = nil, default_values = nil)
|
33
|
-
parts = Group.new(:all, nil)
|
34
|
-
ss = StringScanner.new(path)
|
35
|
-
current_group = parts
|
36
|
-
while !ss.eos?
|
37
|
-
part = ss.scan(@split_regex)
|
38
|
-
case part[0]
|
39
|
-
when ?*, ?:
|
40
|
-
type = part.slice!(0).chr.to_sym
|
41
|
-
current_group << Usher::Route::Variable.new(type, part, requirements && requirements[part.to_sym])
|
42
|
-
when ?{
|
43
|
-
pattern = ''
|
44
|
-
count = 1
|
45
|
-
variable = ss.scan(/[:\*]([^,]+),/)
|
46
|
-
until count.zero?
|
47
|
-
regex_part = ss.scan(/\{|\}|[^\{\}]+/)
|
48
|
-
case regex_part[0]
|
49
|
-
when ?{
|
50
|
-
count += 1
|
51
|
-
when ?}
|
52
|
-
count -= 1
|
53
|
-
end
|
54
|
-
pattern << regex_part
|
55
|
-
end
|
56
|
-
pattern.slice!(pattern.length - 1)
|
57
|
-
regex = Regexp.new(pattern)
|
58
|
-
if variable
|
59
|
-
variable_type = variable.slice!(0).chr.to_sym
|
60
|
-
variable_name = variable[0, variable.size - 1].to_sym
|
61
|
-
current_group << Usher::Route::Variable.new(variable_type, variable_name, requirements && requirements[variable_name], regex)
|
62
|
-
else
|
63
|
-
current_group << regex
|
64
|
-
end
|
65
|
-
when ?(
|
66
|
-
new_group = Group.new(:any, current_group)
|
67
|
-
current_group << new_group
|
68
|
-
current_group = new_group
|
69
|
-
when ?)
|
70
|
-
current_group = current_group.parent.type == :one ? current_group.parent.parent : current_group.parent
|
71
|
-
when ?|
|
72
|
-
unless current_group.parent.type == :one
|
73
|
-
detached_group = current_group.parent.pop
|
74
|
-
new_group = Group.new(:one, detached_group.parent)
|
75
|
-
detached_group.parent = new_group
|
76
|
-
detached_group.type = :all
|
77
|
-
new_group << detached_group
|
78
|
-
new_group.parent << new_group
|
79
|
-
end
|
80
|
-
current_group.parent << Group.new(:all, current_group.parent)
|
81
|
-
current_group = current_group.parent.last
|
82
|
-
else
|
83
|
-
current_group << part
|
84
|
-
end
|
85
|
-
end unless !path || path.empty?
|
86
|
-
paths = calc_paths(parts)
|
87
|
-
paths.each do |path|
|
88
|
-
path.each_with_index do |part, index|
|
89
|
-
if part.is_a?(Usher::Route::Variable)
|
90
|
-
part.default_value = default_values[part.name] if default_values
|
91
|
-
|
92
|
-
case part.type
|
93
|
-
when :*
|
94
|
-
part.look_ahead = path[index + 1, path.size].find{|p| !p.is_a?(Usher::Route::Variable) && !@delimiter_chars.include?(p[0])} || nil
|
95
|
-
when :':'
|
96
|
-
part.look_ahead = path[index + 1, path.size].find{|p| @delimiter_chars.include?(p[0])} || @delimiters.first
|
97
|
-
end
|
98
|
-
end
|
99
|
-
end
|
100
|
-
end
|
101
|
-
paths
|
102
|
-
end
|
103
|
-
|
104
|
-
private
|
105
|
-
|
106
|
-
def cartesian_product!(lval, rval)
|
107
|
-
product = []
|
108
|
-
(lval.size * rval.size).times do |index|
|
109
|
-
val = []
|
110
|
-
val.push(*lval[index % lval.size])
|
111
|
-
val.push(*rval[index % rval.size])
|
112
|
-
product << val
|
113
|
-
end
|
114
|
-
lval.replace(product)
|
115
|
-
end
|
116
|
-
|
117
|
-
def calc_paths(parts)
|
118
|
-
if parts.is_a?(Group)
|
119
|
-
paths = [[]]
|
120
|
-
case parts.type
|
121
|
-
when :all
|
122
|
-
parts.each do |p|
|
123
|
-
cartesian_product!(paths, calc_paths(p))
|
124
|
-
end
|
125
|
-
when :any
|
126
|
-
parts.each do |p|
|
127
|
-
cartesian_product!(paths, calc_paths(p))
|
128
|
-
end
|
129
|
-
paths.unshift([])
|
130
|
-
when :one
|
131
|
-
cartesian_product!(paths, parts.collect do |p|
|
132
|
-
calc_paths(p)
|
133
|
-
end)
|
134
|
-
end
|
135
|
-
paths.each{|p| p.compact!; p.flatten! }
|
136
|
-
paths
|
137
|
-
else
|
138
|
-
[[parts]]
|
139
|
-
end
|
140
|
-
|
141
|
-
end
|
142
|
-
end
|
143
|
-
|
144
|
-
class Group < Array
|
145
|
-
attr_accessor :type
|
146
|
-
attr_accessor :parent
|
147
|
-
|
148
|
-
def inspect
|
149
|
-
"#{type}->#{super}"
|
150
|
-
end
|
151
|
-
|
152
|
-
def initialize(type, parent)
|
153
|
-
@type = type
|
154
|
-
@parent = parent
|
155
|
-
end
|
156
19
|
end
|
157
20
|
|
158
21
|
end
|
data/lib/usher/util.rb
ADDED
@@ -0,0 +1,129 @@
|
|
1
|
+
require 'rack'
|
2
|
+
|
3
|
+
unless Rack::Utils.respond_to?(:uri_escape)
|
4
|
+
module Rack
|
5
|
+
|
6
|
+
module Utils
|
7
|
+
|
8
|
+
def uri_escape(s)
|
9
|
+
s.to_s.gsub(/([^:\/?\[\]\-_~\.!\$&'\(\)\*\+,;=@a-zA-Z0-9]+)/n) {
|
10
|
+
'%'<<$1.unpack('H2'*$1.size).join('%').upcase
|
11
|
+
}.tr(' ', '+')
|
12
|
+
end
|
13
|
+
module_function :uri_escape
|
14
|
+
|
15
|
+
def uri_unescape(s)
|
16
|
+
gsub(/((?:%[0-9a-fA-F]{2})+)/n){
|
17
|
+
[$1.delete('%')].pack('H*')
|
18
|
+
}
|
19
|
+
end
|
20
|
+
module_function :uri_unescape
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class Usher
|
27
|
+
module Util
|
28
|
+
class Generators
|
29
|
+
|
30
|
+
class URL
|
31
|
+
|
32
|
+
attr_accessor :usher
|
33
|
+
|
34
|
+
def generate_full(routing_lookup, request, params = nil)
|
35
|
+
path = path_for_routing_lookup(routing_lookup, params)
|
36
|
+
result = generate_start(path, request)
|
37
|
+
result << generate_path(path, params)
|
38
|
+
end
|
39
|
+
|
40
|
+
def generate(routing_lookup, params = nil)
|
41
|
+
generate_path(path_for_routing_lookup(routing_lookup, params), params)
|
42
|
+
end
|
43
|
+
|
44
|
+
def generate_start(path, request)
|
45
|
+
result = (path.route.generate_with && path.route.generate_with.scheme || request.scheme).dup
|
46
|
+
result << '://'
|
47
|
+
result << (path.route.generate_with && path.route.generate_with.host) ? path.route.generate_with.host : request.host
|
48
|
+
port = path.route.generate_with && path.route.generate_with.port || request.port
|
49
|
+
if result[4] == ?s
|
50
|
+
result << ':' << port.to_s unless port == 443
|
51
|
+
else
|
52
|
+
result << ':' << port.to_s unless port == 80
|
53
|
+
end
|
54
|
+
result
|
55
|
+
end
|
56
|
+
|
57
|
+
def path_for_routing_lookup(routing_lookup, params = {})
|
58
|
+
path = case routing_lookup
|
59
|
+
when Symbol
|
60
|
+
route = @usher.named_routes[routing_lookup]
|
61
|
+
route.find_matching_path(params || {})
|
62
|
+
when Route
|
63
|
+
routing_lookup.find_matching_path(params || {})
|
64
|
+
when nil
|
65
|
+
params.is_a?(Hash) ? @usher.path_for_options(params) : raise
|
66
|
+
when Route::Path
|
67
|
+
routing_lookup
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Generates a completed URL based on a +route+ or set of optional +params+
|
72
|
+
#
|
73
|
+
# set = Usher.new
|
74
|
+
# route = set.add_named_route(:test_route, '/:controller/:action')
|
75
|
+
# set.generate_url(nil, {:controller => 'c', :action => 'a'}) == '/c/a' => true
|
76
|
+
# set.generate_url(:test_route, {:controller => 'c', :action => 'a'}) == '/c/a' => true
|
77
|
+
# set.generate_url(route.primary_path, {:controller => 'c', :action => 'a'}) == '/c/a' => true
|
78
|
+
def generate_path(path, params = nil)
|
79
|
+
raise UnrecognizedException.new unless path
|
80
|
+
|
81
|
+
params = Array(params) if params.is_a?(String)
|
82
|
+
if params.is_a?(Array)
|
83
|
+
given_size = params.size
|
84
|
+
extra_params = params.last.is_a?(Hash) ? params.pop : nil
|
85
|
+
params = Hash[*path.dynamic_parts.inject([]){|a, dynamic_part| a.concat([dynamic_part.name, params.shift || raise(MissingParameterException.new("got #{given_size}, expected #{path.dynamic_parts.size} parameters"))]); a}]
|
86
|
+
params.merge!(extra_params) if extra_params
|
87
|
+
end
|
88
|
+
|
89
|
+
result = ''
|
90
|
+
path.parts.each do |part|
|
91
|
+
case part
|
92
|
+
when Route::Variable::Glob
|
93
|
+
value = (params && params.delete(part.name)) || part.default_value || raise(MissingParameterException.new)
|
94
|
+
value.each_with_index do |current_value, index|
|
95
|
+
part.valid!(current_value)
|
96
|
+
result << current_value.to_s
|
97
|
+
result << '/' if index != value.size - 1
|
98
|
+
end
|
99
|
+
when Route::Variable
|
100
|
+
value = (params && params.delete(part.name)) || part.default_value || raise(MissingParameterException.new)
|
101
|
+
part.valid!(value)
|
102
|
+
result << value.to_s
|
103
|
+
else
|
104
|
+
result << part
|
105
|
+
end
|
106
|
+
end
|
107
|
+
result = Rack::Utils.uri_escape(result)
|
108
|
+
|
109
|
+
unless params.nil? || params.empty?
|
110
|
+
has_query = result[??]
|
111
|
+
params.each do |k,v|
|
112
|
+
case v
|
113
|
+
when Array
|
114
|
+
v.each do |v_part|
|
115
|
+
result << (has_query ? '&' : has_query = true && '?') << Rack::Utils.escape("#{k.to_s}[]") << '=' << Rack::Utils.escape(v_part.to_s)
|
116
|
+
end
|
117
|
+
else
|
118
|
+
result << (has_query ? '&' : has_query = true && '?') << Rack::Utils.escape(k.to_s) << '=' << Rack::Utils.escape(v.to_s)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
result
|
123
|
+
end
|
124
|
+
|
125
|
+
end
|
126
|
+
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|