howl-router 0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.travis.yml +13 -0
- data/Gemfile +3 -0
- data/README.md +132 -0
- data/Rakefile +10 -0
- data/config.ru +9 -0
- data/lib/howl-router.rb +163 -0
- data/lib/howl-router/matcher.rb +46 -0
- data/lib/howl-router/padrino.rb +17 -0
- data/lib/howl-router/padrino/core.rb +41 -0
- data/lib/howl-router/padrino/ext/class_methods.rb +179 -0
- data/lib/howl-router/padrino/ext/instance_methods.rb +60 -0
- data/lib/howl-router/padrino/matcher.rb +8 -0
- data/lib/howl-router/padrino/route.rb +45 -0
- data/lib/howl-router/padrino/router.rb +8 -0
- data/lib/howl-router/request.rb +7 -0
- data/lib/howl-router/route.rb +40 -0
- data/lib/howl-router/router.rb +70 -0
- data/lib/howl-router/version.rb +3 -0
- data/test/helper.rb +83 -0
- data/test/howl_test.rb +101 -0
- data/test/padrino_test.rb +1918 -0
- metadata +165 -0
@@ -0,0 +1,179 @@
|
|
1
|
+
|
2
|
+
class Howl
|
3
|
+
module Padrino
|
4
|
+
module ClassMethods
|
5
|
+
CONTENT_TYPE_ALIASES = { :htm => :html } unless defined?(CONTENT_TYPE_ALIASES)
|
6
|
+
ROUTE_PRIORITY = {:high => 0, :normal => 1, :low => 2} unless defined?(ROUTE_PRIORITY)
|
7
|
+
|
8
|
+
def router
|
9
|
+
@router ||= ::Howl::Padrino::Core.new
|
10
|
+
block_given? ? yield(@router) : @router
|
11
|
+
end
|
12
|
+
|
13
|
+
def compiled_router
|
14
|
+
if @deferred_routes
|
15
|
+
deferred_routes.each { |routes| routes.each { |(route, dest)| route.to(&dest) } }
|
16
|
+
@deferred_routes = nil
|
17
|
+
end
|
18
|
+
router
|
19
|
+
end
|
20
|
+
|
21
|
+
def deferred_routes
|
22
|
+
@deferred_routes ||= ROUTE_PRIORITY.map{[]}
|
23
|
+
end
|
24
|
+
|
25
|
+
def url(*args)
|
26
|
+
params = args.extract_options!
|
27
|
+
names, params_array = args.partition{|a| a.is_a?(Symbol)}
|
28
|
+
name = names.join("_").to_sym
|
29
|
+
if params.is_a?(Hash)
|
30
|
+
params[:format] = params[:format].to_s unless params[:format].nil?
|
31
|
+
params = value_to_param(params)
|
32
|
+
end
|
33
|
+
url = if params_array.empty?
|
34
|
+
compiled_router.path(name, params)
|
35
|
+
else
|
36
|
+
compiled_router.path(name, *(params_array << params))
|
37
|
+
end
|
38
|
+
url[0,0] = conform_uri(uri_root) if defined?(uri_root)
|
39
|
+
url[0,0] = conform_uri(ENV['RACK_BASE_URI']) if ENV['RACK_BASE_URI']
|
40
|
+
url = "/" if url.blank?
|
41
|
+
url
|
42
|
+
rescue Howl::InvalidRouteException
|
43
|
+
route_error = "route mapping for url(#{name.inspect}) could not be found!"
|
44
|
+
raise ::Padrino::Routing::UnrecognizedException.new(route_error)
|
45
|
+
end
|
46
|
+
alias :url_for :url
|
47
|
+
|
48
|
+
def recognize_path(path)
|
49
|
+
responses = @router.recognize_path(path)
|
50
|
+
[responses[0], responses[1]]
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
def route(verb, path, *args, &block)
|
55
|
+
options = case args.size
|
56
|
+
when 2
|
57
|
+
args.last.merge(:map => args.first)
|
58
|
+
when 1
|
59
|
+
map = args.shift if args.first.is_a?(String)
|
60
|
+
if args.first.is_a?(Hash)
|
61
|
+
map ? args.first.merge(:map => map) : args.first
|
62
|
+
else
|
63
|
+
{:map => map || args.first}
|
64
|
+
end
|
65
|
+
when 0
|
66
|
+
{}
|
67
|
+
else raise
|
68
|
+
end
|
69
|
+
|
70
|
+
route_options = options.dup
|
71
|
+
route_options[:provides] = @_provides if @_provides
|
72
|
+
|
73
|
+
if allow_disabled_csrf
|
74
|
+
unless route_options[:csrf_protection] == false
|
75
|
+
route_options[:csrf_protection] = true
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
path, *route_options[:with] = path if path.is_a?(Array)
|
80
|
+
action = path
|
81
|
+
path, name, route_parents, options, route_options = *parse_route(path, route_options, verb)
|
82
|
+
options.reverse_merge!(@_conditions) if @_conditions
|
83
|
+
|
84
|
+
method_name = "#{verb} #{path}"
|
85
|
+
unbound_method = generate_method(method_name, &block)
|
86
|
+
|
87
|
+
block = block.arity != 0 ?
|
88
|
+
proc {|a,p| unbound_method.bind(a).call(*p) } :
|
89
|
+
proc {|a,p| unbound_method.bind(a).call }
|
90
|
+
|
91
|
+
invoke_hook(:route_added, verb, path, block)
|
92
|
+
|
93
|
+
# Howl route construction
|
94
|
+
path[0, 0] = "/" if path == "(.:format)"
|
95
|
+
route = router.add(verb.downcase.to_sym, path, route_options)
|
96
|
+
route.name = name if name
|
97
|
+
route.action = action
|
98
|
+
priority_name = options.delete(:priority) || :normal
|
99
|
+
priority = ROUTE_PRIORITY[priority_name] or raise("Priority #{priority_name} not recognized, try #{ROUTE_PRIORITY.keys.join(', ')}")
|
100
|
+
route.cache = options.key?(:cache) ? options.delete(:cache) : @_cache
|
101
|
+
route.parent = route_parents ? (route_parents.count == 1 ? route_parents.first : route_parents) : route_parents
|
102
|
+
route.host = options.delete(:host) if options.key?(:host)
|
103
|
+
route.user_agent = options.delete(:agent) if options.key?(:agent)
|
104
|
+
if options.key?(:default_values)
|
105
|
+
defaults = options.delete(:default_values)
|
106
|
+
route.default_values = defaults if defaults
|
107
|
+
end
|
108
|
+
options.delete_if do |option, captures|
|
109
|
+
if route.significant_variable_names.include?(option)
|
110
|
+
route.capture[option] = Array(captures).first
|
111
|
+
true
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
# Add Sinatra conditions
|
116
|
+
options.each {|o, a| route.respond_to?("#{o}=") ? route.send("#{o}=", a) : send(o, *a) }
|
117
|
+
conditions, @conditions = @conditions, []
|
118
|
+
route.custom_conditions.concat(conditions)
|
119
|
+
|
120
|
+
invoke_hook(:padrino_route_added, route, verb, path, args, options, block)
|
121
|
+
|
122
|
+
# Add Application defaults
|
123
|
+
route.before_filters.concat(@filters[:before])
|
124
|
+
route.after_filters.concat(@filters[:after])
|
125
|
+
if @_controller
|
126
|
+
route.use_layout = @layout
|
127
|
+
route.controller = Array(@_controller)[0].to_s
|
128
|
+
end
|
129
|
+
|
130
|
+
deferred_routes[priority] << [route, block]
|
131
|
+
|
132
|
+
route
|
133
|
+
end
|
134
|
+
|
135
|
+
def provides(*types)
|
136
|
+
@_use_format = true
|
137
|
+
condition do
|
138
|
+
mime_types = types.map {|t| mime_type(t) }.compact
|
139
|
+
url_format = params[:format].to_sym if params[:format]
|
140
|
+
accepts = request.accept.map {|a| a.to_str }
|
141
|
+
|
142
|
+
# per rfc2616-sec14:
|
143
|
+
# Assume */* if no ACCEPT header is given.
|
144
|
+
catch_all = (accepts.delete "*/*" || accepts.empty?)
|
145
|
+
matching_types = accepts.empty? ? mime_types.slice(0,1) : (accepts & mime_types)
|
146
|
+
if matching_types.empty? && types.include?(:any)
|
147
|
+
matching_types = accepts
|
148
|
+
end
|
149
|
+
|
150
|
+
if !url_format && matching_types.first
|
151
|
+
type = ::Rack::Mime::MIME_TYPES.find {|k, v| v == matching_types.first }[0].sub(/\./,'').to_sym
|
152
|
+
accept_format = CONTENT_TYPE_ALIASES[type] || type
|
153
|
+
elsif catch_all && !types.include?(:any)
|
154
|
+
type = types.first
|
155
|
+
accept_format = CONTENT_TYPE_ALIASES[type] || type
|
156
|
+
end
|
157
|
+
|
158
|
+
matched_format = types.include?(:any) ||
|
159
|
+
types.include?(accept_format) ||
|
160
|
+
types.include?(url_format) ||
|
161
|
+
((!url_format) && request.accept.empty? && types.include?(:html))
|
162
|
+
# per rfc2616-sec14:
|
163
|
+
# answer with 406 if accept is given but types to not match any
|
164
|
+
# provided type
|
165
|
+
halt 406 if
|
166
|
+
(!url_format && !accepts.empty? && !matched_format) ||
|
167
|
+
(settings.respond_to?(:treat_format_as_accept) && settings.treat_format_as_accept && url_format && !matched_format)
|
168
|
+
|
169
|
+
if matched_format
|
170
|
+
@_content_type = url_format || accept_format || :html
|
171
|
+
content_type(@_content_type, :charset => 'utf-8')
|
172
|
+
end
|
173
|
+
|
174
|
+
matched_format
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
|
2
|
+
class Howl
|
3
|
+
module Padrino
|
4
|
+
module InstanceMethods
|
5
|
+
private
|
6
|
+
def route!(base = settings, pass_block = nil)
|
7
|
+
Thread.current['padrino.instance'] = self
|
8
|
+
code, headers, routes = base.compiled_router.call(@request.env)
|
9
|
+
|
10
|
+
status(code)
|
11
|
+
if code == 200
|
12
|
+
routes.each_with_index do |route_pair, index|
|
13
|
+
route = route_pair[0]
|
14
|
+
next if route.user_agent && !(route.user_agent =~ @request.user_agent)
|
15
|
+
original_params, parent_layout, successful = @params.dup, @layout, false
|
16
|
+
|
17
|
+
howl_params = route_pair[1]
|
18
|
+
param_names = route.matcher.names.dup
|
19
|
+
captured_params = howl_params[:captures].is_a?(Array) ? howl_params.delete(:captures) : howl_params.values_at(*param_names)
|
20
|
+
|
21
|
+
@route = request.route_obj = route
|
22
|
+
@params.merge!(howl_params) if howl_params.is_a?(Hash)
|
23
|
+
@params.merge!(:captures => captured_params) unless captured_params.empty?
|
24
|
+
@block_params = howl_params
|
25
|
+
|
26
|
+
filter! :before if index == 0
|
27
|
+
|
28
|
+
catch(:pass) do
|
29
|
+
begin
|
30
|
+
(route.before_filters - settings.filters[:before]).each{|block| instance_eval(&block) }
|
31
|
+
@layout = route.use_layout if route.use_layout
|
32
|
+
route.custom_conditions.each {|block| pass if block.bind(self).call == false } unless route.custom_conditions.empty?
|
33
|
+
halt_response = catch(:halt){ route_eval{ route.block[self, captured_params] }}
|
34
|
+
successful = true
|
35
|
+
halt(halt_response)
|
36
|
+
ensure
|
37
|
+
(route.after_filters - settings.filters[:after]).each {|block| instance_eval(&block) } if successful
|
38
|
+
@layout, @params = parent_layout, original_params
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
else
|
43
|
+
route_eval do
|
44
|
+
headers.each{|k, v| response[k] = v } unless headers.empty?
|
45
|
+
route_missing if code == 404
|
46
|
+
route_missing if allow = response['Allow'] and allow.include?(request.env['REQUEST_METHOD'])
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
if base.superclass.respond_to?(:router)
|
51
|
+
route!(base.superclass, pass_block)
|
52
|
+
return
|
53
|
+
end
|
54
|
+
|
55
|
+
route_eval(&pass_block) if pass_block
|
56
|
+
route_missing
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'howl-router/route'
|
2
|
+
|
3
|
+
class Howl
|
4
|
+
module Padrino
|
5
|
+
class Route < ::Howl::Route
|
6
|
+
attr_accessor :action, :cache, :parent, :use_layout, :controller, :user_agent
|
7
|
+
|
8
|
+
def before_filters(&block)
|
9
|
+
@_before_filters ||= []
|
10
|
+
@_before_filters << block if block_given?
|
11
|
+
@_before_filters
|
12
|
+
end
|
13
|
+
|
14
|
+
def after_filters(&block)
|
15
|
+
@_after_filters ||= []
|
16
|
+
@_after_filters << block if block_given?
|
17
|
+
@_after_filters
|
18
|
+
end
|
19
|
+
|
20
|
+
def custom_conditions(&block)
|
21
|
+
@_custom_conditions ||= []
|
22
|
+
@_custom_conditions << block if block_given?
|
23
|
+
@_custom_conditions
|
24
|
+
end
|
25
|
+
|
26
|
+
def call(app, *args)
|
27
|
+
@block.call(app, *args)
|
28
|
+
end
|
29
|
+
|
30
|
+
def request_methods
|
31
|
+
[verb.to_s.upcase]
|
32
|
+
end
|
33
|
+
|
34
|
+
def significant_variable_names
|
35
|
+
@significant_variable_names ||= if @path.is_a?(String)
|
36
|
+
@path.scan(/(^|[^\\])[:\*]([a-zA-Z0-9_]+)/).map{|p| p.last.to_sym}
|
37
|
+
elsif @path.is_a?(Regexp) and @path.respond_to?(:named_captures)
|
38
|
+
@path.named_captures.keys.map(&:to_sym)
|
39
|
+
else
|
40
|
+
[]
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
class Howl
|
2
|
+
class Route
|
3
|
+
attr_accessor :block, :capture, :router, :params, :name,
|
4
|
+
:order, :default_values, :path_for_generation, :verb
|
5
|
+
|
6
|
+
def initialize(path, options = {}, &block)
|
7
|
+
@path = path
|
8
|
+
@params = {}
|
9
|
+
@capture = {}
|
10
|
+
@order = 0
|
11
|
+
@block = block if block_given?
|
12
|
+
end
|
13
|
+
|
14
|
+
def matcher
|
15
|
+
@matcher ||= Matcher.new(@path, :capture => @capture,
|
16
|
+
:default_values => @default_values)
|
17
|
+
end
|
18
|
+
|
19
|
+
def arity
|
20
|
+
@block.arity
|
21
|
+
end
|
22
|
+
|
23
|
+
def call(*args)
|
24
|
+
@block.call(*args)
|
25
|
+
end
|
26
|
+
|
27
|
+
def to(&block)
|
28
|
+
@block = block if block_given?
|
29
|
+
@order = @router.current_order
|
30
|
+
@router.increment_order
|
31
|
+
end
|
32
|
+
|
33
|
+
def path(*args)
|
34
|
+
return @path if args.empty?
|
35
|
+
params = args[0]
|
36
|
+
params.delete(:captures)
|
37
|
+
matcher.expand(params) if matcher.mustermann?
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
class Howl
|
2
|
+
class Router
|
3
|
+
attr_reader :current_order, :routes, :routes_with_verbs
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
reset!
|
7
|
+
end
|
8
|
+
|
9
|
+
def recognize(request)
|
10
|
+
path_info, verb, request_params = request.is_a?(Hash) ? [request['PATH_INFO'], request['REQUEST_METHOD'], {}] :
|
11
|
+
[request.path_info, request.request_method, request.params]
|
12
|
+
verb = verb.downcase.to_sym
|
13
|
+
ignore_slash_path_info = path_info
|
14
|
+
ignore_slash_path_info = path_info[0..-2] if path_info != "/" and path_info[-1] == "/"
|
15
|
+
|
16
|
+
# Convert hash key into symbol.
|
17
|
+
request_params = request_params.inject({}) do |result, entry|
|
18
|
+
result[entry[0].to_sym] = entry[1]
|
19
|
+
result
|
20
|
+
end
|
21
|
+
|
22
|
+
all_matched_routes = @routes.select do |route|
|
23
|
+
matcher = route.matcher
|
24
|
+
matcher.match(matcher.mustermann? ? ignore_slash_path_info : path_info)
|
25
|
+
end
|
26
|
+
raise NotFound if all_matched_routes.empty?
|
27
|
+
|
28
|
+
raise_method_not_allowed(request, all_matched_routes) unless routes_with_verbs.has_key?(verb)
|
29
|
+
result = all_matched_routes.map{|route|
|
30
|
+
next unless verb == route.verb
|
31
|
+
params, matcher = {}, route.matcher
|
32
|
+
match_data = matcher.match(matcher.mustermann? ? ignore_slash_path_info : path_info)
|
33
|
+
if match_data.names.empty?
|
34
|
+
params[:captures] = match_data.captures
|
35
|
+
else
|
36
|
+
params.merge!(route.params).merge!(match_data.names.inject({}){|result, name|
|
37
|
+
result[name.to_sym] = match_data[name]
|
38
|
+
result
|
39
|
+
}).merge!(request_params){|key, self_value, new_value| self_value || new_value }
|
40
|
+
end
|
41
|
+
[route, params]
|
42
|
+
}.compact
|
43
|
+
raise_method_not_allowed(request, all_matched_routes) if result.empty?
|
44
|
+
result
|
45
|
+
end
|
46
|
+
|
47
|
+
def increment_order
|
48
|
+
@current_order += 1
|
49
|
+
end
|
50
|
+
|
51
|
+
def compile
|
52
|
+
return if @current_order.zero?
|
53
|
+
@routes_with_verbs.each_value{|routes_with_verb|
|
54
|
+
routes_with_verb.sort!{|a, b| a.order <=> b.order }
|
55
|
+
}
|
56
|
+
@routes.sort!{|a, b| a.order <=> b.order }
|
57
|
+
end
|
58
|
+
|
59
|
+
def reset!
|
60
|
+
@routes = []
|
61
|
+
@routes_with_verbs = {}
|
62
|
+
@current_order = 0
|
63
|
+
end
|
64
|
+
|
65
|
+
def raise_method_not_allowed(request, matched_routes)
|
66
|
+
request.acceptable_methods = matched_routes.map(&:verb)
|
67
|
+
raise MethodNotAllowed
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
data/test/helper.rb
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'bundler/setup'
|
2
|
+
ENV['PADRINO_ENV'] = 'test'
|
3
|
+
PADRINO_ROOT = File.dirname(__FILE__) unless defined?(PADRINO_ROOT)
|
4
|
+
require File.expand_path('../../lib/howl-router', __FILE__)
|
5
|
+
|
6
|
+
require 'minitest/unit'
|
7
|
+
require 'minitest/autorun'
|
8
|
+
require 'minitest/spec'
|
9
|
+
require 'mocha/setup'
|
10
|
+
require 'padrino-core'
|
11
|
+
require 'rack'
|
12
|
+
require 'rack/test'
|
13
|
+
|
14
|
+
begin
|
15
|
+
require 'ruby-debug'
|
16
|
+
rescue LoadError; end
|
17
|
+
|
18
|
+
class Sinatra::Base
|
19
|
+
include MiniTest::Assertions
|
20
|
+
end
|
21
|
+
|
22
|
+
class MiniTest::Spec
|
23
|
+
include Rack::Test::Methods
|
24
|
+
|
25
|
+
def howl
|
26
|
+
@app = Howl.new
|
27
|
+
end
|
28
|
+
|
29
|
+
def mock_app(base = nil, &block)
|
30
|
+
@app = Sinatra.new(base || ::Padrino::Application, &block)
|
31
|
+
end
|
32
|
+
|
33
|
+
def app
|
34
|
+
Rack::Lint.new(@app)
|
35
|
+
end
|
36
|
+
|
37
|
+
def method_missing(name, *args, &block)
|
38
|
+
if response && response.respond_to?(name)
|
39
|
+
response.send(name, *args, &block)
|
40
|
+
else
|
41
|
+
super(name, *args, &block)
|
42
|
+
end
|
43
|
+
rescue Rack::Test::Error # no response yet
|
44
|
+
super(name, *args, &block)
|
45
|
+
end
|
46
|
+
alias response last_response
|
47
|
+
|
48
|
+
class << self
|
49
|
+
alias :setup :before unless defined?(Rails)
|
50
|
+
alias :teardown :after unless defined?(Rails)
|
51
|
+
alias :should :it
|
52
|
+
alias :context :describe
|
53
|
+
def should_eventually(desc)
|
54
|
+
it("should eventually #{desc}") { skip("Should eventually #{desc}") }
|
55
|
+
end
|
56
|
+
end
|
57
|
+
alias :assert_no_match :refute_match
|
58
|
+
alias :assert_not_nil :refute_nil
|
59
|
+
alias :assert_not_equal :refute_equal
|
60
|
+
end
|
61
|
+
|
62
|
+
|
63
|
+
class ColoredIO
|
64
|
+
def initialize(io)
|
65
|
+
@io = io
|
66
|
+
end
|
67
|
+
|
68
|
+
def print(o)
|
69
|
+
case o
|
70
|
+
when "." then @io.send(:print, o.green)
|
71
|
+
when "E" then @io.send(:print, o.red)
|
72
|
+
when "F" then @io.send(:print, o.yellow)
|
73
|
+
when "S" then @io.send(:print, o.magenta)
|
74
|
+
else @io.send(:print, o)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def puts(*o)
|
79
|
+
super
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
MiniTest::Unit.output = ColoredIO.new($stdout)
|