howl-router 0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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)
|