usher 0.4.8 → 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 +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
|