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.
@@ -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)