joshbuddy-usher 0.4.8 → 0.4.10

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/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.3'
12
+ s.add_dependency 'fuzzyhash', '>=0.0.5'
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
- :minor: 4
3
- :patch: 8
2
+ :patch: 10
4
3
  :major: 0
4
+ :minor: 4
@@ -8,7 +8,7 @@ class Usher
8
8
 
9
9
  def initialize(&blk)
10
10
  @routes = Usher.new(:request_methods => [:method, :host, :port, :scheme])
11
- @generator = Usher::Generators::URL.new(@routes)
11
+ @generator = Usher::Util::Generators::URL.new(@routes)
12
12
  instance_eval(&blk) if blk
13
13
  end
14
14
 
@@ -13,7 +13,7 @@ class Usher
13
13
 
14
14
  def reset!
15
15
  @usher ||= Usher.new
16
- @url_generator ||= Usher::Generators::URL.new(@usher)
16
+ @url_generator ||= Usher::Util::Generators::URL.new(@usher)
17
17
  @module ||= Module.new
18
18
  @module.instance_methods.each do |selector|
19
19
  @module.class_eval { remove_method selector }
@@ -73,7 +73,7 @@ class Usher
73
73
 
74
74
  def reset!
75
75
  @router = Usher.new
76
- @url_generator = Usher::Generators::URL.new(@router)
76
+ @url_generator = Usher::Util::Generators::URL.new(@router)
77
77
  @configuration_files = []
78
78
  @module ||= Module.new
79
79
  @controller_route_added = false
data/lib/usher/node.rb CHANGED
@@ -6,13 +6,14 @@ class Usher
6
6
 
7
7
  Response = Struct.new(:path, :params)
8
8
 
9
- attr_reader :lookup
9
+ attr_reader :lookup, :greedy_lookup
10
10
  attr_accessor :terminates, :exclusive_type, :parent, :value, :request_methods, :globs_capture_separators
11
11
 
12
12
  def initialize(parent, value)
13
13
  @parent = parent
14
14
  @value = value
15
15
  @lookup = Hash.new
16
+ @greedy_lookup = Hash.new
16
17
  @exclusive_type = nil
17
18
  end
18
19
 
@@ -20,10 +21,18 @@ class Usher
20
21
  @lookup = FuzzyHash.new(@lookup)
21
22
  end
22
23
 
24
+ def upgrade_greedy_lookup
25
+ @greedy_lookup = FuzzyHash.new(@greedy_lookup)
26
+ end
27
+
23
28
  def depth
24
29
  @depth ||= @parent && @parent.is_a?(Node) ? @parent.depth + 1 : 0
25
30
  end
26
31
 
32
+ def greedy?
33
+ !@greedy_lookup.empty?
34
+ end
35
+
27
36
  def self.root(route_set, request_methods, globs_capture_separators)
28
37
  root = self.new(route_set, nil)
29
38
  root.request_methods = request_methods
@@ -69,16 +78,29 @@ class Usher
69
78
  end
70
79
  else
71
80
  key.globs_capture_separators = globs_capture_separators if key.is_a?(Route::Variable)
72
-
73
- if !key.is_a?(Route::Variable)
81
+
82
+ case key
83
+ 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
91
+ 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
98
+ end
99
+ else
74
100
  current_node.upgrade_lookup if key.is_a?(Regexp)
75
101
  current_node.lookup[key] ||= Node.new(current_node, key)
76
- elsif key.regex_matcher
77
- current_node.upgrade_lookup
78
- current_node.lookup[key.regex_matcher] ||= Node.new(current_node, key)
79
- else
80
- current_node.lookup[nil] ||= Node.new(current_node, key)
81
- end
102
+ end
103
+
82
104
  end
83
105
  current_node = target_node
84
106
  end
@@ -87,29 +109,38 @@ class Usher
87
109
  route
88
110
  end
89
111
 
90
- def find(usher, request, path, params = [])
112
+ def find(usher, request, original_path, path, params = [], position = 0)
91
113
  if exclusive_type
92
114
  [lookup[request.send(exclusive_type)], lookup[nil]].each do |n|
93
- if n && (ret = n.find(usher, request, path.dup, params.dup))
115
+ if n && (ret = n.find(usher, request, original_path, path.dup, params.dup, position))
94
116
  return ret
95
117
  end
96
118
  end
97
119
  elsif path.size.zero? && terminates?
98
120
  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])))
122
+ position += matched_part.size
123
+ params << [next_path.value.name, whole_path.slice!(0, matched_part.size)]
124
+ next_path.find(usher, request, original_path, whole_path.size.zero? ? whole_path : usher.splitter.url_split(whole_path), params, position)
99
125
  elsif !path.size.zero? && (next_part = lookup[part = path.shift] || lookup[nil])
126
+ position += part.size
100
127
  case next_part.value
101
128
  when Route::Variable
102
129
  case next_part.value.type
103
130
  when :*
104
131
  params << [next_part.value.name, []] unless params.last && params.last.first == next_part.value.name
105
132
  loop do
106
- if (next_part.value.look_ahead === part || (!usher.splitter.delimiter_chars.include?(part[0]) && next_part.value.regex_matcher && !next_part.value.regex_matcher.match(part)))
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)))
107
134
  path.unshift(part)
108
- path.unshift(next_part.parent.value) if usher.splitter.delimiter_chars.include?(next_part.parent.value[0])
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
109
140
  break
110
141
  elsif next_part.value.globs_capture_separators
111
142
  params.last.last << part
112
- elsif !usher.splitter.delimiter_chars.include?(part[0])
143
+ elsif !usher.delimiter_chars.include?(part[0])
113
144
  next_part.value.valid!(part)
114
145
  params.last.last << part
115
146
  end
@@ -124,11 +155,13 @@ class Usher
124
155
  var.valid!(part)
125
156
  params << [var.name, part]
126
157
  until (var.look_ahead === path.first) || path.empty?
127
- params.last.last << path.shift
158
+ next_path_part = path.shift
159
+ position += next_path_part.size
160
+ params.last.last << next_path_part
128
161
  end
129
162
  end
130
163
  end
131
- next_part.find(usher, request, path, params)
164
+ next_part.find(usher, request, original_path, path, params, position)
132
165
  else
133
166
  nil
134
167
  end
@@ -16,6 +16,10 @@ class Usher
16
16
  "#{type}#{name}"
17
17
  end
18
18
 
19
+ def greedy?
20
+ type == :'!'
21
+ end
22
+
19
23
  def valid!(val)
20
24
  case @validator
21
25
  when Proc
data/lib/usher/route.rb CHANGED
@@ -4,18 +4,17 @@ require File.join(File.dirname(__FILE__), 'route', 'request_method')
4
4
 
5
5
  class Usher
6
6
  class Route
7
- attr_reader :paths, :original_path, :requirements, :conditions, :destination, :named, :generate_with
7
+ attr_reader :paths, :requirements, :conditions, :destination, :named, :generate_with
8
8
 
9
9
  GenerateWith = Struct.new(:scheme, :port, :host)
10
10
 
11
- def initialize(original_path, router, conditions, requirements, default_values, generate_with) # :nodoc:
12
- @original_path = original_path
11
+ def initialize(parsed_paths, router, conditions, requirements, default_values, generate_with) # :nodoc:
13
12
  @router = router
14
13
  @requirements = requirements
15
14
  @conditions = conditions
16
15
  @default_values = default_values
17
- @paths = @router.splitter.split(@original_path, @requirements, @default_values).collect {|path| Path.new(self, path)}
18
16
  @generate_with = GenerateWith.new(generate_with[:scheme], generate_with[:port], generate_with[:host]) if generate_with
17
+ @paths = parsed_paths.collect {|path| Path.new(self, path)}
19
18
  end
20
19
 
21
20
  def grapher
@@ -3,156 +3,19 @@ require 'strscan'
3
3
  class Usher
4
4
  class Splitter
5
5
 
6
- def self.for_delimiters(delimiters, valid_regex)
7
- delimiters_regex = delimiters.collect{|d| Regexp.quote(d)} * '|'
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)}}]|[^#{router.delimiters.collect{|d| Regexp.quote(d)}}]+"))
13
8
  end
14
-
15
- attr_reader :paths
16
9
 
17
10
  class SplitterInstance
18
-
19
- attr_reader :delimiter_chars
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
@@ -0,0 +1,133 @@
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
+ def initialize(usher)
33
+ @usher = usher
34
+ end
35
+
36
+ def generate_full(routing_lookup, request, params = nil)
37
+ path = path_for_routing_lookup(routing_lookup, params)
38
+ result = generate_start(path, request)
39
+ result << generate_path(path, params)
40
+ end
41
+
42
+ def generate(routing_lookup, params = nil)
43
+ generate_path(path_for_routing_lookup(routing_lookup, params), params)
44
+ end
45
+
46
+ def generate_start(path, request)
47
+ result = (path.route.generate_with && path.route.generate_with.scheme || request.scheme).dup
48
+ result << '://'
49
+ result << (path.route.generate_with && path.route.generate_with.host) ? path.route.generate_with.host : request.host
50
+ port = path.route.generate_with && path.route.generate_with.port || request.port
51
+ if result[4] == ?s
52
+ result << ':' << port.to_s if port != 443
53
+ else
54
+ result << ':' << port.to_s if port != 80
55
+ end
56
+ result
57
+ end
58
+
59
+ def path_for_routing_lookup(routing_lookup, params)
60
+ path = case routing_lookup
61
+ when Symbol
62
+ route = @usher.named_routes[routing_lookup]
63
+ params.is_a?(Hash) ? route.find_matching_path(params) : route.paths.first
64
+ when Route
65
+ params.is_a?(Hash) ? routing_lookup.find_matching_path(params) : routing_lookup.paths.first
66
+ when nil
67
+ params.is_a?(Hash) ? @usher.path_for_options(params) : raise
68
+ when Route::Path
69
+ routing_lookup
70
+ end
71
+ end
72
+
73
+ # Generates a completed URL based on a +route+ or set of optional +params+
74
+ #
75
+ # set = Usher.new
76
+ # route = set.add_named_route(:test_route, '/:controller/:action')
77
+ # set.generate_url(nil, {:controller => 'c', :action => 'a'}) == '/c/a' => true
78
+ # set.generate_url(:test_route, {:controller => 'c', :action => 'a'}) == '/c/a' => true
79
+ # set.generate_url(route.primary_path, {:controller => 'c', :action => 'a'}) == '/c/a' => true
80
+ def generate_path(path, params = nil)
81
+ raise UnrecognizedException.new unless path
82
+
83
+ params = Array(params) if params.is_a?(String)
84
+ if params.is_a?(Array)
85
+ given_size = params.size
86
+ extra_params = params.last.is_a?(Hash) ? params.pop : nil
87
+ 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}]
88
+ params.merge!(extra_params) if extra_params
89
+ end
90
+
91
+ result = ''
92
+ path.parts.each do |part|
93
+ case part
94
+ when Route::Variable
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
108
+ end
109
+ else
110
+ result << part
111
+ end
112
+ end
113
+ result = Rack::Utils.uri_escape(result)
114
+
115
+ if params && !params.empty?
116
+ has_query = result[??]
117
+ params.each do |k,v|
118
+ case v
119
+ when Array
120
+ v.each do |v_part|
121
+ result << (has_query ? '&' : has_query = true && '?') << Rack::Utils.escape("#{k.to_s}[]") << '=' << Rack::Utils.escape(v_part.to_s)
122
+ end
123
+ else
124
+ result << (has_query ? '&' : has_query = true && '?') << Rack::Utils.escape(k.to_s) << '=' << Rack::Utils.escape(v.to_s)
125
+ end
126
+ end
127
+ end
128
+ result
129
+ end
130
+ end
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,148 @@
1
+ require 'strscan'
2
+
3
+ class Usher
4
+ module Util
5
+ class Parser
6
+
7
+ def self.for_delimiters(router, valid_regex)
8
+ ParserInstance.new(
9
+ router,
10
+ Regexp.new('((:|\*)?' + valid_regex + '|' + router.delimiters_regex + '|\(|\)|\||\{)')
11
+ )
12
+ end
13
+
14
+ class ParserInstance
15
+
16
+ def initialize(router, split_regex)
17
+ @router = router
18
+ @split_regex = split_regex
19
+ end
20
+
21
+ def parse(path, requirements = nil, default_values = nil)
22
+ parts = Group.new(:all, nil)
23
+ ss = StringScanner.new(path)
24
+ current_group = parts
25
+ while !ss.eos?
26
+ part = ss.scan(@split_regex)
27
+ 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])
31
+ when ?{
32
+ pattern = ''
33
+ count = 1
34
+ variable = ss.scan(/[!:\*]([^,]+),/)
35
+ until count.zero?
36
+ regex_part = ss.scan(/\{|\}|[^\{\}]+/)
37
+ case regex_part[0]
38
+ when ?{
39
+ count += 1
40
+ when ?}
41
+ count -= 1
42
+ end
43
+ pattern << regex_part
44
+ end
45
+ pattern.slice!(pattern.length - 1)
46
+ regex = Regexp.new(pattern)
47
+ if variable
48
+ variable_type = variable.slice!(0).chr.to_sym
49
+ 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)
51
+ else
52
+ current_group << regex
53
+ end
54
+ when ?(
55
+ new_group = Group.new(:any, current_group)
56
+ current_group << new_group
57
+ current_group = new_group
58
+ when ?)
59
+ current_group = current_group.parent.type == :one ? current_group.parent.parent : current_group.parent
60
+ when ?|
61
+ unless current_group.parent.type == :one
62
+ detached_group = current_group.parent.pop
63
+ new_group = Group.new(:one, detached_group.parent)
64
+ detached_group.parent = new_group
65
+ detached_group.type = :all
66
+ new_group << detached_group
67
+ new_group.parent << new_group
68
+ end
69
+ current_group.parent << Group.new(:all, current_group.parent)
70
+ current_group = current_group.parent.last
71
+ else
72
+ current_group << part
73
+ end
74
+ 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)
104
+ end
105
+
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
+ end
132
+
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
+ end
147
+ end
148
+ end
data/lib/usher/util.rb ADDED
@@ -0,0 +1,6 @@
1
+ class Usher
2
+ module Util
3
+ autoload :Generators, File.join(File.dirname(__FILE__), 'util', 'generate')
4
+ autoload :Parser, File.join(File.dirname(__FILE__), 'util', 'parser')
5
+ end
6
+ end
data/lib/usher.rb CHANGED
@@ -4,12 +4,12 @@ require File.join(File.dirname(__FILE__), 'usher', 'grapher')
4
4
  require File.join(File.dirname(__FILE__), 'usher', 'interface')
5
5
  require File.join(File.dirname(__FILE__), 'usher', 'splitter')
6
6
  require File.join(File.dirname(__FILE__), 'usher', 'exceptions')
7
+ require File.join(File.dirname(__FILE__), 'usher', 'util')
7
8
 
8
9
  class Usher
9
10
 
10
- autoload :Generators, File.join(File.dirname(__FILE__), 'usher', 'generate')
11
11
 
12
- attr_reader :tree, :named_routes, :route_count, :routes, :splitter, :delimiters
12
+ attr_reader :tree, :named_routes, :route_count, :routes, :splitter, :delimiters, :delimiter_chars, :delimiters_regex
13
13
 
14
14
  SymbolArraySorter = proc {|a,b| a.hash <=> b.hash} #:nodoc:
15
15
 
@@ -54,12 +54,18 @@ class Usher
54
54
  def initialize(options = nil)
55
55
  @globs_capture_separators = options && options.key?(:globs_capture_separators) ? options.delete(:globs_capture_separators) : false
56
56
  @delimiters = options && options.delete(:delimiters) || ['/', '.']
57
+ @delimiter_chars = @delimiters.collect{|d| d[0]}
58
+ @delimiters_regex = @delimiters.collect{|d| Regexp.quote(d)} * '|'
57
59
  @valid_regex = options && options.delete(:valid_regex) || '[0-9A-Za-z\$\-_\+!\*\',]+'
58
60
  @request_methods = options && options.delete(:request_methods) || [:protocol, :domain, :port, :query_string, :remote_ip, :user_agent, :referer, :method, :subdomains]
59
- @splitter = Splitter.for_delimiters(@delimiters, @valid_regex)
61
+ @splitter = Splitter.for_delimiters(self, @valid_regex)
60
62
  reset!
61
63
  end
62
64
 
65
+ def parser
66
+ @parser ||= Util::Parser.for_delimiters(self, @valid_regex)
67
+ end
68
+
63
69
  # Adds a route referencable by +name+. Sett add_route for format +path+ and +options+.
64
70
  #
65
71
  # set = Usher.new
@@ -152,7 +158,18 @@ class Usher
152
158
  end
153
159
  end
154
160
  end
155
- route = Route.new(path, self, conditions, requirements, default_values, generate_with)
161
+
162
+ path = parser.parse(path, requirements, default_values) if path.is_a?(String)
163
+
164
+ route = Route.new(
165
+ path,
166
+ self,
167
+ conditions,
168
+ requirements,
169
+ default_values,
170
+ generate_with
171
+ )
172
+
156
173
  route.to(options) if options && !options.empty?
157
174
 
158
175
  @tree.add(route)
@@ -169,7 +186,7 @@ class Usher
169
186
  # route = set.add_route('/test')
170
187
  # set.recognize(Request.new('/test')).path.route == route => true
171
188
  def recognize(request, path = request.path)
172
- @tree.find(self, request, @splitter.url_split(path))
189
+ @tree.find(self, request, path, @splitter.url_split(path))
173
190
  end
174
191
 
175
192
  # Recognizes a set of +parameters+ and gets the closest matching Usher::Route::Path or +nil+ if no route exists.
@@ -6,7 +6,7 @@ describe "Usher URL generation" do
6
6
  before(:each) do
7
7
  @route_set = Usher.new
8
8
  @route_set.reset!
9
- @url_generator = Usher::Generators::URL.new(@route_set)
9
+ @url_generator = Usher::Util::Generators::URL.new(@route_set)
10
10
  end
11
11
 
12
12
  it "should generate a simple URL" do
@@ -6,7 +6,7 @@ describe "Usher grapher" do
6
6
  before(:each) do
7
7
  @route_set = Usher.new
8
8
  @route_set.reset!
9
- @url_generator = Usher::Generators::URL.new(@route_set)
9
+ @url_generator = Usher::Util::Generators::URL.new(@route_set)
10
10
  end
11
11
 
12
12
  it "should find a simple path" do
@@ -0,0 +1,75 @@
1
+ require 'lib/usher'
2
+
3
+ describe "Usher route tokenizing" do
4
+
5
+
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']]
8
+ end
9
+
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']]
12
+ end
13
+
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']]
16
+ end
17
+
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"]]
20
+ end
21
+
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']]
24
+ end
25
+
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 == [
28
+ ['/', 'test', '/', 'this'],
29
+ ['/', 'test', '/', 'this', '/', 'split']
30
+ ]
31
+ end
32
+
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 == [
35
+ ['/', 'test', '/', 'this','/', 'split'],
36
+ ['/', 'test', '/', 'this','/', 'split2']
37
+ ]
38
+ end
39
+
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 == [
42
+ ['/', 'test','/', 'this'],
43
+ ['/', 'test','/', 'this', '/', 'split'],
44
+ ['/', 'test','/', 'this', '/', 'split2']
45
+ ]
46
+ end
47
+
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)') == [
50
+ ['/', "test", '/', "this"],
51
+ ['/', "test", '/', "this", '/', "split"],
52
+ ['/', "test", '/', "this", '/', "split2"],
53
+ ['/', "test", '/', "this", '/', "split", '/', "split2"]
54
+ ]
55
+ end
56
+
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))') == [
59
+ ['/', "test", '/', "this"],
60
+ ['/', "test", '/', "this", '/', "split"],
61
+ ['/', "test", '/', "this", '/', "split", '.', Usher::Route::Variable.new(:':', :format)]
62
+ ]
63
+ end
64
+
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} ==
67
+ [ ':split', '*splitter' ]
68
+ end
69
+
70
+ it "should == variable types" do
71
+ parts = Usher.new(:delimiters => ['/', '.'], :valid_regex => '[0-9A-Za-z\$\-_\+!\*\',]+').parser.parse('/:split/:split').first
72
+ parts[1].should == parts[3]
73
+ end
74
+
75
+ end
@@ -93,6 +93,18 @@ describe "Usher route recognition" do
93
93
  route_set.recognize(build_request({:method => 'get', :path => '/test/part/hello/again/123/hello/again/onemore'})).params.should == [[:test, ['hello', 'again', '123', 'hello', 'again']], [:party, 'onemore']]
94
94
  end
95
95
 
96
+ it "should recgonize a greedy regex single variable" do
97
+ target_route = route_set.add_route('/test/part/{!test,one/more/time}')
98
+ route_set.recognize(build_request({:method => 'get', :path => '/test/part/one/more/time'})).path.route.should == target_route
99
+ route_set.recognize(build_request({:method => 'get', :path => '/test/part/one/more/time'})).params.should == [[:test, 'one/more/time']]
100
+ end
101
+
102
+ it "should recgonize a greedy regex single variable with static parts after" do
103
+ target_route = route_set.add_route('/test/part/{!test,one/more/time}/help')
104
+ route_set.recognize(build_request({:method => 'get', :path => '/test/part/one/more/time/help'})).path.route.should == target_route
105
+ route_set.recognize(build_request({:method => 'get', :path => '/test/part/one/more/time/help'})).params.should == [[:test, 'one/more/time']]
106
+ end
107
+
96
108
  it "should recgonize two glob-style variables separated by a static part" do
97
109
  target_route = route_set.add_route('/*format/innovate/*onemore')
98
110
  response = route_set.recognize(build_request({:method => 'get', :path => '/sample/html/innovate/apple'}))
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.8
4
+ version: 0.4.10
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-06-16 00:00:00 -07:00
12
+ date: 2009-07-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.3
23
+ version: 0.0.5
24
24
  version:
25
25
  description: A general purpose routing library
26
26
  email: joshbuddy@gmail.com
@@ -38,7 +38,6 @@ files:
38
38
  - VERSION.yml
39
39
  - lib/usher.rb
40
40
  - lib/usher/exceptions.rb
41
- - lib/usher/generate.rb
42
41
  - lib/usher/grapher.rb
43
42
  - lib/usher/interface.rb
44
43
  - lib/usher/interface/email_interface.rb
@@ -55,10 +54,14 @@ files:
55
54
  - lib/usher/route/request_method.rb
56
55
  - lib/usher/route/variable.rb
57
56
  - lib/usher/splitter.rb
57
+ - lib/usher/util.rb
58
+ - lib/usher/util/generate.rb
59
+ - lib/usher/util/parser.rb
58
60
  - rails/init.rb
59
61
  - spec/private/email/recognize_spec.rb
60
62
  - spec/private/generate_spec.rb
61
63
  - spec/private/grapher_spec.rb
64
+ - spec/private/parser_spec.rb
62
65
  - spec/private/path_spec.rb
63
66
  - spec/private/rack/dispatch_spec.rb
64
67
  - spec/private/rails2_2/compat.rb
@@ -71,7 +74,6 @@ files:
71
74
  - spec/private/rails2_3/recognize_spec.rb
72
75
  - spec/private/recognize_spec.rb
73
76
  - spec/private/request_method_spec.rb
74
- - spec/private/split_spec.rb
75
77
  - spec/spec.opts
76
78
  has_rdoc: false
77
79
  homepage: http://github.com/joshbuddy/usher
@@ -103,6 +105,7 @@ test_files:
103
105
  - spec/private/email/recognize_spec.rb
104
106
  - spec/private/generate_spec.rb
105
107
  - spec/private/grapher_spec.rb
108
+ - spec/private/parser_spec.rb
106
109
  - spec/private/path_spec.rb
107
110
  - spec/private/rack/dispatch_spec.rb
108
111
  - spec/private/rails2_2/compat.rb
@@ -115,4 +118,3 @@ test_files:
115
118
  - spec/private/rails2_3/recognize_spec.rb
116
119
  - spec/private/recognize_spec.rb
117
120
  - spec/private/request_method_spec.rb
118
- - spec/private/split_spec.rb
@@ -1,131 +0,0 @@
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
- class Generators
28
-
29
- class URL
30
-
31
- def initialize(usher)
32
- @usher = usher
33
- end
34
-
35
- def generate_full(routing_lookup, request, params = nil)
36
- path = path_for_routing_lookup(routing_lookup, params)
37
- result = generate_start(path, request)
38
- result << generate_path(path, params)
39
- end
40
-
41
- def generate(routing_lookup, params = nil)
42
- generate_path(path_for_routing_lookup(routing_lookup, params), params)
43
- end
44
-
45
- def generate_start(path, request)
46
- result = (path.route.generate_with && path.route.generate_with.scheme || request.scheme).dup
47
- result << '://'
48
- result << (path.route.generate_with && path.route.generate_with.host) ? path.route.generate_with.host : request.host
49
- port = path.route.generate_with && path.route.generate_with.port || request.port
50
- if result[4] == ?s
51
- result << ':' << port.to_s if port != 443
52
- else
53
- result << ':' << port.to_s if port != 80
54
- end
55
- result
56
- end
57
-
58
- def path_for_routing_lookup(routing_lookup, params)
59
- path = case routing_lookup
60
- when Symbol
61
- route = @usher.named_routes[routing_lookup]
62
- params.is_a?(Hash) ? route.find_matching_path(params) : route.paths.first
63
- when Route
64
- params.is_a?(Hash) ? routing_lookup.find_matching_path(params) : routing_lookup.paths.first
65
- when nil
66
- params.is_a?(Hash) ? @usher.path_for_options(params) : raise
67
- when Route::Path
68
- routing_lookup
69
- end
70
- end
71
-
72
- # Generates a completed URL based on a +route+ or set of optional +params+
73
- #
74
- # set = Usher.new
75
- # route = set.add_named_route(:test_route, '/:controller/:action')
76
- # set.generate_url(nil, {:controller => 'c', :action => 'a'}) == '/c/a' => true
77
- # set.generate_url(:test_route, {:controller => 'c', :action => 'a'}) == '/c/a' => true
78
- # set.generate_url(route.primary_path, {:controller => 'c', :action => 'a'}) == '/c/a' => true
79
- def generate_path(path, params = nil)
80
- raise UnrecognizedException.new unless path
81
-
82
- params = Array(params) if params.is_a?(String)
83
- if params.is_a?(Array)
84
- given_size = params.size
85
- extra_params = params.last.is_a?(Hash) ? params.pop : nil
86
- 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}]
87
- params.merge!(extra_params) if extra_params
88
- end
89
-
90
- result = ''
91
- path.parts.each do |part|
92
- case part
93
- when Route::Variable
94
- value = (params && params.delete(part.name)) || part.default_value || raise(MissingParameterException.new)
95
- case part.type
96
- when :*
97
- value.each_with_index do |current_value, index|
98
- current_value = current_value.to_s unless current_value.is_a?(String)
99
- part.valid!(current_value)
100
- result << current_value
101
- result << '/' if index != value.size - 1
102
- end
103
- when :':'
104
- value = value.to_s unless value.is_a?(String)
105
- part.valid!(value)
106
- result << value
107
- end
108
- else
109
- result << part
110
- end
111
- end
112
- result = Rack::Utils.uri_escape(result)
113
-
114
- if params && !params.empty?
115
- has_query = result[??]
116
- params.each do |k,v|
117
- case v
118
- when Array
119
- v.each do |v_part|
120
- result << (has_query ? '&' : has_query = true && '?') << Rack::Utils.escape("#{k.to_s}[]") << '=' << Rack::Utils.escape(v_part.to_s)
121
- end
122
- else
123
- result << (has_query ? '&' : has_query = true && '?') << Rack::Utils.escape(k.to_s) << '=' << Rack::Utils.escape(v.to_s)
124
- end
125
- end
126
- end
127
- result
128
- end
129
- end
130
- end
131
- end
@@ -1,76 +0,0 @@
1
- require 'lib/usher'
2
-
3
- describe "Usher route tokenizing" do
4
-
5
-
6
- it "should split / delimited routes" do
7
- Usher::Splitter.for_delimiters(['/', '.'], '[0-9A-Za-z\$\-_\+!\*\',]+').split('/test/this/split').should == [['/', 'test', '/','this', '/', 'split']]
8
- end
9
-
10
- it "should split / delimited routes with a regex in it" do
11
- Usher::Splitter.for_delimiters(['/', '.'], '[0-9A-Za-z\$\-_\+!\*\',]+').
12
- split('/test/{this}/split').should == [['/', 'test', '/', /this/, '/', 'split']]
13
- end
14
-
15
- it "should split on ' ' delimited routes as well" do
16
- Usher::Splitter.for_delimiters([' '], '[0-9A-Za-z\$\-_\+!\*\',]+').split('test this split').should == [['test', ' ', 'this', ' ', 'split']]
17
- end
18
-
19
- it "should split on email delimiters as well" do
20
- Usher::Splitter.for_delimiters(['@', '+', '-', '.'], '[a-zA-Z0-9]+').split('one+more.12345-09876-alphanum3ric5@domain.com').should == [["one", '+', "more", ".", "12345", '-', "09876", '-', "alphanum3ric5", "@", "domain", ".", "com"]]
21
- end
22
-
23
- it "should split on ' ' delimited routes for more complex routes as well" do
24
- Usher::Splitter.for_delimiters([' '], '[0-9A-Za-z\$\-_\+!\*\',]+').split('(test|this) split').should == [['test', ' ', 'split'], ['this', ' ', 'split']]
25
- end
26
-
27
- it "should group optional parts with brackets" do
28
- Usher::Splitter.for_delimiters(['/', '.'], '[0-9A-Za-z\$\-_\+!\*\',]+').split('/test/this(/split)').should == [
29
- ['/', 'test', '/', 'this'],
30
- ['/', 'test', '/', 'this', '/', 'split']
31
- ]
32
- end
33
-
34
- it "should group exclusive optional parts with brackets and pipes" do
35
- Usher::Splitter.for_delimiters(['/', '.'], '[0-9A-Za-z\$\-_\+!\*\',]+').split('/test/this(/split|/split2)').should == [
36
- ['/', 'test', '/', 'this','/', 'split'],
37
- ['/', 'test', '/', 'this','/', 'split2']
38
- ]
39
- end
40
-
41
- it "should group exclusive optional-optional parts with brackets and pipes" do
42
- Usher::Splitter.for_delimiters(['/', '.'], '[0-9A-Za-z\$\-_\+!\*\',]+').split('/test/this((/split|/split2))').should == [
43
- ['/', 'test','/', 'this'],
44
- ['/', 'test','/', 'this', '/', 'split'],
45
- ['/', 'test','/', 'this', '/', 'split2']
46
- ]
47
- end
48
-
49
- it "should group optional parts with brackets (for non overlapping groups)" do
50
- Usher::Splitter.for_delimiters(['/', '.'], '[0-9A-Za-z\$\-_\+!\*\',]+').split('/test/this(/split)(/split2)') == [
51
- ['/', "test", '/', "this"],
52
- ['/', "test", '/', "this", '/', "split"],
53
- ['/', "test", '/', "this", '/', "split2"],
54
- ['/', "test", '/', "this", '/', "split", '/', "split2"]
55
- ]
56
- end
57
-
58
- it "should group nested-optional parts with brackets" do
59
- Usher::Splitter.for_delimiters(['/', '.'], '[0-9A-Za-z\$\-_\+!\*\',]+').split('/test/this(/split(.:format))') == [
60
- ['/', "test", '/', "this"],
61
- ['/', "test", '/', "this", '/', "split"],
62
- ['/', "test", '/', "this", '/', "split", '.', Usher::Route::Variable.new(:':', :format)]
63
- ]
64
- end
65
-
66
- it "should to_s all different variable types" do
67
- Usher::Splitter.for_delimiters(['/', '.'], '[0-9A-Za-z\$\-_\+!\*\',]+').split('/:split/*splitter').first.collect{|v| v.to_s} ==
68
- [ ':split', '*splitter' ]
69
- end
70
-
71
- it "should == variable types" do
72
- parts = Usher::Splitter.for_delimiters(['/', '.'], '[0-9A-Za-z\$\-_\+!\*\',]+').split('/:split/:split').first
73
- parts[1].should == parts[3]
74
- end
75
-
76
- end