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 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.5'
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
@@ -1,4 +1,4 @@
1
1
  ---
2
- :patch: 11
3
2
  :major: 0
4
- :minor: 4
3
+ :minor: 5
4
+ :patch: 1
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
- unless path.dynamic_keys.size.zero?
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 { |r|
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]+', :globs_capture_separators => true)
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, :globs_capture_separators
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, globs_capture_separators)
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
- if key.greedy?
85
- if key.regex_matcher
86
- current_node.upgrade_greedy_lookup
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
- if key.regex_matcher
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? && ((next_path, matched_part) = greedy_lookup.match_with_result(whole_path = original_path[position, original_path.size])))
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
- case next_part.value.type
130
- when :*
131
- params << [next_part.value.name, []] unless params.last && params.last.first == next_part.value.name
132
- loop do
133
- 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)))
134
- path.unshift(part)
135
- position -= part.size
136
- if usher.delimiter_chars.include?(next_part.parent.value[0])
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
- when :':'
154
- var = next_part.value
155
- var.valid!(part)
156
- params << [var.name, part]
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
@@ -2,22 +2,48 @@ class Usher
2
2
  class Route
3
3
  class Path
4
4
 
5
- attr_reader :dynamic_parts, :dynamic_map, :dynamic_indicies, :route, :parts, :dynamic_required_keys, :dynamic_keys
5
+ attr_reader :route, :parts
6
6
 
7
7
  def initialize(route, parts)
8
8
  @route = route
9
9
  @parts = parts
10
- @dynamic_indicies = []
11
- @parts.each_index{|i| @dynamic_indicies << i if @parts[i].is_a?(Variable)}
12
- @dynamic_parts = @parts.values_at(*@dynamic_indicies)
13
- @dynamic_map = {}
14
- @dynamic_parts.each{|p| @dynamic_map[p.name] = p }
15
- @dynamic_keys = @dynamic_map.keys
16
- @dynamic_required_keys = @dynamic_parts.select{|dp| !dp.default_value}.map{|dp| dp.name}
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
- (@dynamic_required_keys - keys).size.zero?
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
@@ -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, :globs_capture_separators, :default_value
5
+ attr_accessor :look_ahead, :default_value
6
6
 
7
- def initialize(type, name, validator = nil, regex_matcher = nil, globs_capture_separators = false)
8
- @type = type
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 #{@validator}, root cause #{e.inspect}")
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.type == @type && o.name == @name && o.validator == @validator)
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
@@ -1,4 +1,5 @@
1
1
  require File.join(File.dirname(__FILE__), 'route', 'path')
2
+ require File.join(File.dirname(__FILE__), 'route', 'util')
2
3
  require File.join(File.dirname(__FILE__), 'route', 'variable')
3
4
  require File.join(File.dirname(__FILE__), 'route', 'request_method')
4
5
 
@@ -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
@@ -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
- case part.type
97
- when :*
98
- value.each_with_index do |current_value, index|
99
- current_value = current_value.to_s unless current_value.is_a?(String)
100
- part.valid!(current_value)
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
@@ -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
- type = part.slice!(0).chr.to_sym
30
- current_group << Usher::Route::Variable.new(type, part, requirements && requirements[part.to_sym])
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 << Usher::Route::Variable.new(variable_type, variable_name, requirements && requirements[variable_name], regex)
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.type == :one ? current_group.parent.parent : 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.type == :one
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.type = :all
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
- paths = calc_paths(parts)
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, @globs_capture_separators)
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(path, options = nil)
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
- path = parser.parse(path, requirements, default_values) if path.is_a?(String)
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
- path,
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@*domain') { |params| receiver.action(params) }
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.{:id,^\d+$}-{:sid,^\d+$}-{:tok,^\w+$}@*domain') { |params| receiver.action(params) }
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
 
@@ -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.parse('/test/this/split').should == [['/', 'test', '/','this', '/', 'split']]
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.parse('/test/{this}/split').should == [['/', 'test', '/', /this/, '/', 'split']]
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.parse('test this split').should == [['test', ' ', 'this', ' ', 'split']]
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.parse('one+more.12345-09876-alphanum3ric5@domain.com').should == [["one", '+', "more", ".", "12345", '-', "09876", '-', "alphanum3ric5", "@", "domain", ".", "com"]]
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.parse('(test|this) split').should == [['test', ' ', 'split'], ['this', ' ', 'split']]
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.parse('/test/this(/split)').should == [
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.parse('/test/this(/split|/split2)').should == [
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.parse('/test/this((/split|/split2))').should == [
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.parse('/test/this(/split)(/split2)') == [
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.parse('/test/this(/split(.:format))') == [
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(:':', :format)]
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.parse('/:split/*splitter').first.collect{|v| v.to_s} ==
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.parse('/:split/:split').first
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.11
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-07-22 00:00:00 -07:00
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.5
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.2.0
102
+ rubygems_version: 1.3.5
101
103
  signing_key:
102
104
  specification_version: 3
103
105
  summary: A general purpose routing library