usher 0.5.13 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -89,6 +89,7 @@ For instance, the path, <tt>/path/something(.xml|.html)</tt> would only match <t
89
89
  * +requirements+ - After transformation, tests the condition using ===. If it returns false, it raises an <tt>Usher::ValidationException</tt>
90
90
  * +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.
91
91
  * +default_values+ - Provides values for variables in your route for generation. If you're using URL generation, then any values supplied here that aren't included in your path will be appended to the query string.
92
+ * +priority+ - If there are two routes which equally match, the route with the highest priority will match first.
92
93
  * Any other key is interpreted as a requirement for the variable of its name.
93
94
 
94
95
  == Rails
@@ -123,6 +124,21 @@ For instance, the path, <tt>/path/something(.xml|.html)</tt> would only match <t
123
124
  >> curl http://127.0.0.1:3000/hello/samueltanders
124
125
  << Hi there samueltanders
125
126
 
127
+
128
+ == Sinatra
129
+
130
+ In Sinatra, you get the extra method, +generate+, which lets you generate a url. Name your routes with <tt>:name</tt> when you define them.
131
+
132
+ require 'rubygems'
133
+ require 'usher'
134
+ require 'sinatra'
135
+
136
+ Usher::Interface.for(:sinatra)
137
+
138
+ get '/hi', :name => :hi do
139
+ "Hello World! #{generate(:hi)}"
140
+ end
141
+
126
142
  == DONE
127
143
 
128
144
  * add support for () optional parts
data/Rakefile CHANGED
@@ -11,7 +11,7 @@ begin
11
11
  s.homepage = "http://github.com/joshbuddy/usher"
12
12
  s.authors = ["Joshua Hull", 'Jakub Šťastný', 'Daniel Neighman', 'Daniel Vartanov'].sort
13
13
  s.files = FileList["[A-Z]*", "{lib,spec,rails}/**/*"]
14
- s.add_dependency 'fuzzyhash', '>=0.0.9'
14
+ s.add_dependency 'fuzzyhash', '>=0.0.11'
15
15
  s.rubyforge_project = 'joshbuddy-usher'
16
16
  end
17
17
  Jeweler::GemcutterTasks.new
data/VERSION.yml CHANGED
@@ -1,5 +1,5 @@
1
1
  ---
2
- :major: 0
3
2
  :build:
4
- :minor: 5
5
- :patch: 13
3
+ :patch: 0
4
+ :major: 0
5
+ :minor: 6
data/lib/usher.rb CHANGED
@@ -1,4 +1,4 @@
1
- $: << File.expand_path(File.dirname(__FILE__))
1
+ $LOAD_PATH << File.expand_path(File.dirname(__FILE__))
2
2
  require File.join('usher', 'node')
3
3
  require File.join('usher', 'route')
4
4
  require File.join('usher', 'grapher')
@@ -6,7 +6,6 @@ require File.join('usher', 'interface')
6
6
  require File.join('usher', 'splitter')
7
7
  require File.join('usher', 'exceptions')
8
8
  require File.join('usher', 'util')
9
- require File.join('usher', 'spinoffs', 'strscan_additions')
10
9
  require File.join('usher', 'delimiters')
11
10
 
12
11
  class Usher
@@ -26,6 +25,8 @@ class Usher
26
25
  @routes.empty?
27
26
  end
28
27
 
28
+ # Returns the number of routes
29
+ #
29
30
  def route_count
30
31
  @routes.size
31
32
  end
@@ -41,7 +42,7 @@ class Usher
41
42
  @root = Node.root(self, request_methods)
42
43
  @named_routes = {}
43
44
  @routes = []
44
- @grapher = Grapher.new
45
+ @grapher = Grapher.new(self)
45
46
  @priority_lookups = false
46
47
  end
47
48
  alias clear! reset!
@@ -55,14 +56,34 @@ class Usher
55
56
  #
56
57
  # <tt>:request_methods</tt>: Array of Symbols. (default <tt>[:protocol, :domain, :port, :query_string, :remote_ip, :user_agent, :referer, :method, :subdomains]</tt>)
57
58
  # Array of methods called against the request object for the purposes of matching route requirements.
59
+ #
60
+ # <tt>:generator</tt>: +nil+ or Generator instance. (default: +nil+) Take a look at <tt>Usher::Util::Generators for examples.</tt>.
61
+ #
62
+ # <tt>:ignore_trailing_delimiters</tt>: +true+ or +false+. (default: +false+) Ignore trailing delimiters in recognizing paths.
63
+ #
64
+ # <tt>:consider_destination_keys</tt>: +true+ or +false+. (default: +false+) When generating, and using hash destinations, you can have
65
+ # Usher use the destination hash to match incoming params.
66
+ #
67
+ # Example, you create a route with a destination of :controller => 'test', :action => 'action'. If you made a call to generator with :controller => 'test',
68
+ # :action => 'action', it would pick that route to use for generation.
58
69
  def initialize(options = nil)
59
- self.generator = options && options.delete(:generator)
60
- self.delimiters = Delimiters.new(options && options.delete(:delimiters) || ['/', '.'])
61
- self.valid_regex = options && options.delete(:valid_regex) || '[0-9A-Za-z\$\-_\+!\*\',]+'
62
- self.request_methods = options && options.delete(:request_methods)
70
+ self.generator = options && options.delete(:generator)
71
+ self.delimiters = Delimiters.new(options && options.delete(:delimiters) || ['/', '.'])
72
+ self.valid_regex = options && options.delete(:valid_regex) || '[0-9A-Za-z\$\-_\+!\*\',]+'
73
+ self.request_methods = options && options.delete(:request_methods)
74
+ self.ignore_trailing_delimiters = options && options.key?(:ignore_trailing_delimiters) ? options.delete(:ignore_trailing_delimiters) : false
75
+ self.consider_destination_keys = options && options.key?(:consider_destination_keys) ? options.delete(:consider_destination_keys) : false
63
76
  reset!
64
77
  end
65
78
 
79
+ def ignore_trailing_delimiters?
80
+ @ignore_trailing_delimiters
81
+ end
82
+
83
+ def consider_destination_keys?
84
+ @consider_destination_keys
85
+ end
86
+
66
87
  def parser
67
88
  @parser ||= Util::Parser.for_delimiters(self, valid_regex)
68
89
  end
@@ -172,6 +193,7 @@ class Usher
172
193
  # * +requirements+ - After transformation, tests the condition using ===. If it returns false, it raises an <tt>Usher::ValidationException</tt>
173
194
  # * +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.
174
195
  # * +default_values+ - Provides values for variables in your route for generation. If you're using URL generation, then any values supplied here that aren't included in your path will be appended to the query string.
196
+ # * +priority+ - If there are two routes which equally match, the route with the highest priority will match first.
175
197
  # * Any other key is interpreted as a requirement for the variable of its name.
176
198
  def add_route(path, options = nil)
177
199
  route = get_route(path, options)
@@ -252,7 +274,7 @@ class Usher
252
274
 
253
275
  private
254
276
 
255
- attr_accessor :request_methods
277
+ attr_accessor :request_methods, :ignore_trailing_delimiters, :consider_destination_keys
256
278
  attr_reader :valid_regex
257
279
 
258
280
  def generator=(generator)
@@ -308,7 +330,7 @@ class Usher
308
330
  end
309
331
 
310
332
  def rebuild_grapher!
311
- @grapher = Grapher.new
333
+ @grapher = Grapher.new(self)
312
334
  @routes.each{|r| @grapher.add_route(r)}
313
335
  end
314
336
  end
@@ -1,22 +1,24 @@
1
- class Delimiters < Array
1
+ class Usher
2
+ class Delimiters < Array
2
3
 
3
- attr_reader :unescaped
4
+ attr_reader :unescaped
4
5
 
5
- def initialize(ary)
6
- super ary
7
- @unescaped = self.map do |delimiter|
8
- (delimiter[0] == ?\\) ? delimiter[1..-1] : delimiter
6
+ def initialize(ary)
7
+ super ary
8
+ @unescaped = self.map do |delimiter|
9
+ (delimiter[0] == ?\\) ? delimiter[1..-1] : delimiter
10
+ end
9
11
  end
10
- end
11
12
 
12
- def first_in(array)
13
- # TODO: should we optimize this O(n*m)? hash or modified or KNP or at leaset sort + b-search. But they are so short
13
+ def first_in(array)
14
+ # TODO: should we optimize this O(n*m)? hash or modified or KNP or at leaset sort + b-search. But they are so short
14
15
 
15
- array.each do |element|
16
- return element if self.unescaped.any? { |delimiter| delimiter == element }
16
+ array.each do |element|
17
+ return element if self.unescaped.any? { |delimiter| delimiter == element }
18
+ end
19
+ nil
17
20
  end
18
- nil
19
- end
20
21
 
21
- # TODO: Delimiters#regex and so on
22
+ # TODO: Delimiters#regex and so on
23
+ end
22
24
  end
data/lib/usher/grapher.rb CHANGED
@@ -1,7 +1,10 @@
1
1
  class Usher
2
2
  class Grapher
3
3
 
4
- def initialize
4
+ attr_reader :routes, :router, :orders, :key_count, :cache
5
+
6
+ def initialize(router)
7
+ @router = router
5
8
  reset!
6
9
  end
7
10
 
@@ -10,49 +13,69 @@ class Usher
10
13
  @orders = Hash.new{|h,k| h[k] = Hash.new{|h2, k2| h2[k2] = []}}
11
14
  @key_count = Hash.new(0)
12
15
  @cache = {}
16
+ @routes = []
13
17
  end
14
18
 
15
19
  def add_route(route)
16
- route.paths.each do |path|
17
- if path.dynamic?
18
- path.dynamic_keys.each do |k|
19
- @orders[path.dynamic_keys.size][k] << path
20
- @key_count[k] += 1
21
- end
22
-
23
- dynamic_parts_with_defaults = path.dynamic_parts.select{|part| part.default_value }.map{|dp| dp.name}
24
- dynamic_parts_without_defaults = path.dynamic_parts.select{|part| !part.default_value }.map{|dp| dp.name}
20
+ routes << route
21
+ end
22
+
23
+ def process_routes
24
+ return if @processed
25
+ routes.each do |route|
26
+ route.paths.each do |path|
27
+ if path.dynamic?
28
+ path.dynamic_keys.each do |k|
29
+ orders[path.dynamic_keys.size][k] << path
30
+ key_count[k] += 1
31
+ end
32
+
33
+ dynamic_parts_with_defaults = path.dynamic_parts.select{|part| part.default_value }.map{|dp| dp.name}
34
+ dynamic_parts_without_defaults = path.dynamic_parts.select{|part| !part.default_value }.map{|dp| dp.name}
25
35
 
26
- (1...(2 ** (dynamic_parts_with_defaults.size))).each do |i|
27
- current_set = dynamic_parts_without_defaults.dup
28
- dynamic_parts_with_defaults.each_with_index do |dp, index|
29
- current_set << dp unless (index & i) == 0
36
+ (1...(2 ** (dynamic_parts_with_defaults.size))).each do |i|
37
+ current_set = dynamic_parts_without_defaults.dup
38
+ dynamic_parts_with_defaults.each_with_index do |dp, index|
39
+ current_set << dp unless (index & i) == 0
40
+ end
41
+
42
+ current_set.each do |k|
43
+ orders[current_set.size][k] << path
44
+ key_count[k] += 1
45
+ end
30
46
  end
31
47
 
32
- current_set.each do |k|
33
- @orders[current_set.size][k] << path
34
- @key_count[k] += 1
48
+ end
49
+
50
+ if router.consider_destination_keys?
51
+ path.route.destination_keys.each do |k|
52
+ orders[path.route.destination_keys.size][k] << path
53
+ key_count[k] += 1
35
54
  end
36
55
  end
37
56
  end
38
57
  end
58
+ @processed = true
39
59
  end
40
60
 
41
61
  def significant_keys
42
- @significant_keys ||= @key_count.keys.uniq
62
+ @significant_keys ||= key_count.keys.uniq
43
63
  end
44
64
 
45
65
  def find_matching_path(params)
46
66
  unless params.empty?
67
+ process_routes
47
68
  set = params.keys & significant_keys
48
- if cached = @cache[set]
69
+ if cached = cache[set]
49
70
  return cached
50
71
  end
51
72
  set.size.downto(1) do |o|
52
73
  set.each do |k|
53
- @orders[o][k].each do |r|
54
- if r.can_generate_from?(set)
55
- @cache[set] = r
74
+ orders[o][k].each do |r|
75
+ if r.can_generate_from_keys?(set)
76
+ cache[set] = r
77
+ return r
78
+ elsif router.consider_destination_keys? && r.can_generate_from_params?(params)
56
79
  return r
57
80
  end
58
81
  end
@@ -15,18 +15,22 @@ class Usher
15
15
  register(:rack, File.join(File.dirname(__FILE__), 'interface', 'rack'))
16
16
  register(:rails3, File.join(File.dirname(__FILE__), 'interface', 'rails3'))
17
17
  register(:text, File.join(File.dirname(__FILE__), 'interface', 'text'))
18
+ register(:sinatra, File.join(File.dirname(__FILE__), 'interface', 'sinatra'))
18
19
 
19
- # Usher::Interface.for(:rack, &block)
20
- def self.for(name, &block)
20
+ def self.class_for(name)
21
21
  name = name.to_sym
22
22
  if InterfaceRegistry[name]
23
23
  require InterfaceRegistry[name]
24
- const = Usher::Interface.const_get(File.basename(InterfaceRegistry[name]).to_s.split(/_/).map{|e| e.capitalize}.join)
25
- const.new(&block)
24
+ Usher::Interface.const_get(File.basename(InterfaceRegistry[name]).to_s.split(/_/).map{|e| e.capitalize}.join)
26
25
  else
27
26
  raise ArgumentError, "Interface #{name.inspect} doesn't exist. Choose one of: #{InterfaceRegistry.keys.inspect}"
28
27
  end
29
28
  end
30
29
 
30
+ # Usher::Interface.for(:rack, &block)
31
+ def self.for(name, *args, &block)
32
+ class_for(name).new(*args, &block)
33
+ end
34
+
31
35
  end
32
36
  end
@@ -0,0 +1,66 @@
1
+ class Usher
2
+ module Interface
3
+ class Sinatra
4
+
5
+ module Extension
6
+
7
+ def self.included(cls)
8
+ cls::Base.class_eval(<<-HERE_DOC, __FILE__, __LINE__)
9
+ def self.route(verb, path, options={}, &block)
10
+ @router ||= Usher.new(:request_methods => [:request_method, :host, :port, :scheme], :generator => Usher::Util::Generators::URL.new)
11
+
12
+ name = options.delete(:name)
13
+ options[:conditions] ||= {}
14
+ options[:conditions][:request_method] = verb
15
+ options[:conditions][:host] = options.delete(:host) if options.key?(:host)
16
+
17
+ # Because of self.options.host
18
+ host_name(options.delete(:host)) if options.key?(:host)
19
+
20
+ define_method "\#{verb} \#{path}", &block
21
+ unbound_method = instance_method("\#{verb} \#{path}")
22
+ block =
23
+ if block.arity != 0
24
+ lambda { unbound_method.bind(self).call(*@block_params) }
25
+ else
26
+ lambda { unbound_method.bind(self).call }
27
+ end
28
+
29
+ invoke_hook(:route_added, verb, path, block)
30
+
31
+ route = @router.add_route(path, options).to(block)
32
+ route.name(name) if name
33
+ route
34
+ end
35
+
36
+ def self.router
37
+ @router
38
+ end
39
+
40
+ def route!(base = self.class)
41
+ if self.class.router and match = self.class.router.recognize(@request)
42
+ @block_params = match.params.map{|p| p.last}
43
+ @params = @params ? @params.merge(match.params_as_hash) : match.params_as_hash
44
+ route_eval(&match.destination)
45
+ elsif base.superclass.respond_to?(:routes)
46
+ route! base.superclass
47
+ else
48
+ route_missing
49
+ end
50
+ end
51
+
52
+ def generate(name, *params)
53
+ self.class.router.generator.generate(name, *params)
54
+ end
55
+
56
+ HERE_DOC
57
+ end
58
+
59
+ end
60
+
61
+ def initialize
62
+ ::Sinatra.send(:include, Extension)
63
+ end
64
+ end
65
+ end
66
+ end
data/lib/usher/node.rb CHANGED
@@ -8,6 +8,14 @@ class Usher
8
8
  def partial_match?
9
9
  !remaining_path.nil?
10
10
  end
11
+
12
+ def params_as_hash
13
+ params.inject({}){|hash, pair| hash[pair.first] = pair.last; hash}
14
+ end
15
+
16
+ def destination
17
+ path && path.route.destination
18
+ end
11
19
  end
12
20
 
13
21
  attr_reader :normal, :greedy, :request
@@ -112,7 +120,7 @@ class Usher
112
120
  end
113
121
 
114
122
  def find(usher, request_object, original_path, path, params = [], position = 0)
115
- if terminates? && (path.empty? || terminates.route.partial_match?)
123
+ if terminates? && (path.empty? || terminates.route.partial_match? || (usher.ignore_trailing_delimiters? && path.all?{|p| usher.delimiters.include?(p)}))
116
124
  terminates.route.partial_match? ?
117
125
  Response.new(terminates, params, original_path[position, original_path.size], original_path[0, position]) :
118
126
  Response.new(terminates, params, nil, original_path)
data/lib/usher/route.rb CHANGED
@@ -22,9 +22,20 @@ class Usher
22
22
  @generate_with = GenerateWith.new(generate_with[:scheme], generate_with[:port], generate_with[:host]) if generate_with
23
23
  end
24
24
 
25
+ def destination_keys
26
+ @destination_keys ||= case
27
+ when Hash
28
+ destination.keys
29
+ when CompoundDestination
30
+ destination.options.keys
31
+ else
32
+ nil
33
+ end
34
+ end
35
+
25
36
  def grapher
26
37
  unless @grapher
27
- @grapher = Grapher.new
38
+ @grapher = Grapher.new(router)
28
39
  @grapher.add_route(self)
29
40
  end
30
41
  @grapher
@@ -53,12 +64,17 @@ class Usher
53
64
  end
54
65
 
55
66
  def find_matching_path(params)
56
- if params.nil? || params.empty?
57
- matching_path = @paths.first
67
+ #if router.find_matching_paths_based_on_destination_keys?
68
+ matching_path = if params.nil? || params.empty?
69
+ @paths.first
58
70
  else
59
- matching_path = @paths.size == 1 ? @paths.first : grapher.find_matching_path(params)
71
+ @paths.size == 1 ? @paths.first : grapher.find_matching_path(params)
60
72
  end
61
-
73
+
74
+ if matching_path.nil? and router.find_matching_paths_based_on_destination_keys?
75
+ # do something
76
+ end
77
+
62
78
  if parent_route
63
79
  matching_path = parent_route.find_matching_path(params).merge(matching_path)
64
80
  matching_path.route = self
@@ -67,21 +83,38 @@ class Usher
67
83
  matching_path
68
84
  end
69
85
 
70
- # Sets +options+ on a route. Returns +self+.
86
+ CompoundDestination = Struct.new(:args, :block, :options)
87
+
88
+ # Sets destination on a route. Returns +self+.
89
+ #
90
+ # This method acceps varargs. If you pass in more than one variable, it will be returned to you wrapped in a +CompoundDestination+.
91
+ # If you send it varargs and the last member is a Hash, it will pop off the hash, and will be stored under <tt>#options</tt>.
92
+ # Otherwise, if you use send a single variable, or call it with a block, these will be returned to you by <tt>#destination</tt>.
71
93
  #
72
94
  # Request = Struct.new(:path)
73
95
  # set = Usher.new
74
96
  # route = set.add_route('/test')
75
97
  # route.to(:controller => 'testing', :action => 'index')
76
98
  # set.recognize(Request.new('/test')).first.params => {:controller => 'testing', :action => 'index'}
77
- def to(options = nil, &block)
78
- raise "cannot set destination as block and argument" if block_given? && options
79
- @destination = if block_given?
80
- block
99
+ #
100
+ #
101
+ #
102
+ def to(*args, &block)
103
+ if !args.empty? && block
104
+ @destination = CompoundDestination.new(args, block, args.last.is_a?(Hash) ? args.pop : {})
105
+ elsif block.nil?
106
+ case args.size
107
+ when 0
108
+ raise "destination should be set as something"
109
+ when 1
110
+ @destination = args.first
111
+ else
112
+ @destination = CompoundDestination.new(args, nil, args.last.is_a?(Hash) ? args.pop : {})
113
+ end
81
114
  else
82
- options.parent_route = self if options.respond_to?(:parent_route=)
83
- options
115
+ @destination = block
84
116
  end
117
+ args.first.parent_route = self if args.first.respond_to?(:parent_route=)
85
118
  self
86
119
  end
87
120