howl-router 0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,8 @@
1
+ require 'howl-router/matcher'
2
+
3
+ class Howl
4
+ module Padrino
5
+ class Matcher < ::Howl::Matcher
6
+ end
7
+ end
8
+ 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,8 @@
1
+ require 'howl-router/router'
2
+
3
+ class Howl
4
+ module Padrino
5
+ class Router < ::Howl::Router
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,7 @@
1
+ require 'rack'
2
+
3
+ class Howl
4
+ class Request < Rack::Request
5
+ attr_accessor :acceptable_methods
6
+ end
7
+ 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
@@ -0,0 +1,3 @@
1
+ class Howl
2
+ VERSION = '0.1'
3
+ 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)