hanami-router 0.0.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,187 @@
1
+ require 'http_router'
2
+ require 'hanami/utils/io'
3
+ require 'hanami/routing/endpoint_resolver'
4
+ require 'hanami/routing/route'
5
+ require 'hanami/routing/parsers'
6
+ require 'hanami/routing/force_ssl'
7
+ require 'hanami/routing/error'
8
+ require 'hanami/utils/path_prefix'
9
+
10
+ Hanami::Utils::IO.silence_warnings do
11
+ HttpRouter::Route::VALID_HTTP_VERBS = %w{GET POST PUT PATCH DELETE HEAD OPTIONS TRACE}
12
+ end
13
+
14
+ module Hanami
15
+ module Routing
16
+ # Invalid route
17
+ # This is raised when the router fails to recognize a route, because of the
18
+ # given arguments.
19
+ #
20
+ # @since 0.1.0
21
+ class InvalidRouteException < Hanami::Routing::Error
22
+ end
23
+
24
+ # HTTP router
25
+ #
26
+ # This implementation is based on ::HttpRouter (http_router gem).
27
+ #
28
+ # Hanami::Router wraps an instance of this class, in order to protect its
29
+ # public API from any future change of ::HttpRouter.
30
+ #
31
+ # @since 0.1.0
32
+ # @api private
33
+ class HttpRouter < ::HttpRouter
34
+ # Script name - rack enviroment variable
35
+ #
36
+ # @since 0.5.0
37
+ # @api private
38
+ SCRIPT_NAME = 'SCRIPT_NAME'.freeze
39
+
40
+ # @since 0.5.0
41
+ # @api private
42
+ attr_reader :namespace
43
+
44
+ # Initialize the router.
45
+ #
46
+ # @see Hanami::Router#initialize
47
+ #
48
+ # @since 0.1.0
49
+ # @api private
50
+ def initialize(options = {}, &blk)
51
+ super(options, &nil)
52
+
53
+ @namespace = options[:namespace] if options[:namespace]
54
+ @default_scheme = options[:scheme] if options[:scheme]
55
+ @default_host = options[:host] if options[:host]
56
+ @default_port = options[:port] if options[:port]
57
+ @route_class = options[:route] || Routing::Route
58
+ @resolver = options[:resolver] || Routing::EndpointResolver.new(options)
59
+ @parsers = Routing::Parsers.new(options[:parsers])
60
+ @prefix = Utils::PathPrefix.new(options[:prefix] || '')
61
+ @force_ssl = Hanami::Routing::ForceSsl.new(!!options[:force_ssl], host: @default_host, port: @default_port)
62
+ end
63
+
64
+ # Separator between controller and action name.
65
+ #
66
+ # @see Hanami::Routing::EndpointResolver::ACTION_SEPARATOR
67
+ #
68
+ # @since 0.1.0
69
+ # @api private
70
+ def action_separator
71
+ @resolver.action_separator
72
+ end
73
+
74
+ # Finds a path from the given options.
75
+ #
76
+ # @see Hanami::Routing::EndpointResolver#find
77
+ #
78
+ # @since 0.1.0
79
+ # @api private
80
+ def find(options)
81
+ @resolver.find(options)
82
+ end
83
+
84
+ # Generate a relative URL for a specified named route.
85
+ #
86
+ # @see Hanami::Router#path
87
+ #
88
+ # @since 0.1.0
89
+ # @api private
90
+ def raw_path(route, *args)
91
+ _rescue_url_recognition do
92
+ _custom_path(super(route, *args))
93
+ end
94
+ end
95
+
96
+ # Generate an absolute URL for a specified named route.
97
+ #
98
+ # @see Hanami::Router#path
99
+ #
100
+ # @since 0.1.0
101
+ # @api private
102
+ def raw_url(route, *args)
103
+ _rescue_url_recognition do
104
+ _custom_path(super(route, *args))
105
+ end
106
+ end
107
+
108
+ # Support for OPTIONS HTTP verb
109
+ #
110
+ # @see Hanami::Router#options
111
+ #
112
+ # @since 0.1.0
113
+ # @api private
114
+ def options(path, options = {}, &blk)
115
+ add_with_request_method(path, :options, options, &blk)
116
+ end
117
+
118
+ # Allow to mount a Rack app
119
+ #
120
+ # @see Hanami::Router#mount
121
+ #
122
+ # @since 0.1.1
123
+ # @api private
124
+ def mount(app, options)
125
+ add("#{ options.fetch(:at) }*").to(
126
+ @resolver.resolve(to: app)
127
+ )
128
+ end
129
+
130
+ # @api private
131
+ def raw_call(env, &blk)
132
+ if response = @force_ssl.call(env)
133
+ response
134
+ else
135
+ super(@parsers.call(env))
136
+ end
137
+ end
138
+
139
+ # @api private
140
+ def reset!
141
+ uncompile
142
+ @routes, @named_routes, @root = [], Hash.new{|h,k| h[k] = []}, Node::Root.new(self)
143
+ @default_host, @default_port, @default_scheme = 'localhost', 80, 'http'
144
+ end
145
+
146
+ # @api private
147
+ def pass_on_response(response)
148
+ super response.to_a
149
+ end
150
+
151
+ # @api private
152
+ def no_response(request, env)
153
+ if request.acceptable_methods.any? && !request.acceptable_methods.include?(env['REQUEST_METHOD'])
154
+ [405, {'Allow' => request.acceptable_methods.sort.join(", ")}, []]
155
+ else
156
+ @default_app.call(env)
157
+ end
158
+ end
159
+
160
+ # @api private
161
+ # @since 0.5.0
162
+ def rewrite_path_info(env, request)
163
+ super
164
+ env[SCRIPT_NAME] = @prefix + env[SCRIPT_NAME]
165
+ end
166
+
167
+ private
168
+
169
+ def _rescue_url_recognition
170
+ yield
171
+ rescue ::HttpRouter::InvalidRouteException,
172
+ ::HttpRouter::TooManyParametersException => e
173
+ raise Routing::InvalidRouteException.new("#{ e.message } - please check given arguments")
174
+ end
175
+
176
+ def add_with_request_method(path, method, opts = {}, &app)
177
+ super.generate(@resolver, opts, &app)
178
+ end
179
+
180
+ def _custom_path(uri_string)
181
+ uri = URI.parse(uri_string)
182
+ uri.path = @prefix.join(uri.path)
183
+ uri.to_s
184
+ end
185
+ end
186
+ end
187
+ end
@@ -0,0 +1,92 @@
1
+ require 'delegate'
2
+ require 'hanami/utils/path_prefix'
3
+
4
+ module Hanami
5
+ module Routing
6
+ # Namespace for routes.
7
+ # Implementation of Hanami::Router#namespace
8
+ #
9
+ # @since 0.1.0
10
+ #
11
+ # @api private
12
+ #
13
+ # @see Hanami::Router#namespace
14
+ class Namespace < SimpleDelegator
15
+ # @api private
16
+ # @since 0.1.0
17
+ def initialize(router, name, &blk)
18
+ @router = router
19
+ @name = Utils::PathPrefix.new(name)
20
+ __setobj__(@router)
21
+ instance_eval(&blk)
22
+ end
23
+
24
+ # @api private
25
+ # @since 0.1.0
26
+ def get(path, options = {}, &endpoint)
27
+ super(@name.join(path), options, &endpoint)
28
+ end
29
+
30
+ # @api private
31
+ # @since 0.1.0
32
+ def post(path, options = {}, &endpoint)
33
+ super(@name.join(path), options, &endpoint)
34
+ end
35
+
36
+ # @api private
37
+ # @since 0.1.0
38
+ def put(path, options = {}, &endpoint)
39
+ super(@name.join(path), options, &endpoint)
40
+ end
41
+
42
+ # @api private
43
+ # @since 0.1.0
44
+ def patch(path, options = {}, &endpoint)
45
+ super(@name.join(path), options, &endpoint)
46
+ end
47
+
48
+ # @api private
49
+ # @since 0.1.0
50
+ def delete(path, options = {}, &endpoint)
51
+ super(@name.join(path), options, &endpoint)
52
+ end
53
+
54
+ # @api private
55
+ # @since 0.1.0
56
+ def trace(path, options = {}, &endpoint)
57
+ super(@name.join(path), options, &endpoint)
58
+ end
59
+
60
+ # @api private
61
+ # @since 0.1.0
62
+ def options(path, options = {}, &endpoint)
63
+ super(@name.join(path), options, &endpoint)
64
+ end
65
+
66
+ # @api private
67
+ # @since 0.1.0
68
+ def resource(name, options = {})
69
+ super name, options.merge(namespace: @name.relative_join(options[:namespace]))
70
+ end
71
+
72
+ # @api private
73
+ # @since 0.1.0
74
+ def resources(name, options = {})
75
+ super name, options.merge(namespace: @name.relative_join(options[:namespace]))
76
+ end
77
+
78
+ # @api private
79
+ # @since 0.1.0
80
+ def redirect(path, options = {}, &endpoint)
81
+ super(@name.join(path), options.merge(to: @name.join(options[:to])), &endpoint)
82
+ end
83
+
84
+ # Supports nested namespaces
85
+ # @api private
86
+ # @since 0.1.0
87
+ def namespace(name, &blk)
88
+ Routing::Namespace.new(self, name, &blk)
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,71 @@
1
+ require 'hanami/routing/parsing/parser'
2
+
3
+ module Hanami
4
+ module Routing
5
+ class Parsers
6
+ CONTENT_TYPE = 'CONTENT_TYPE'.freeze
7
+ MEDIA_TYPE_MATCHER = /\s*[;,]\s*/.freeze
8
+
9
+ RACK_INPUT = 'rack.input'.freeze
10
+ ROUTER_PARAMS = 'router.params'.freeze
11
+
12
+ def initialize(parsers)
13
+ @parsers = prepare(parsers)
14
+ _redefine_call
15
+ end
16
+
17
+ def call(env)
18
+ env
19
+ end
20
+
21
+ private
22
+ def prepare(args)
23
+ result = Hash.new
24
+ args = Array(args)
25
+ return result if args.empty?
26
+
27
+ args.each do |arg|
28
+ parser = Parsing::Parser.for(arg)
29
+
30
+ parser.mime_types.each do |mime|
31
+ result[mime] = parser
32
+ end
33
+ end
34
+
35
+ result.default = Parsing::Parser.new
36
+ result
37
+ end
38
+
39
+ def _redefine_call
40
+ return if @parsers.empty?
41
+
42
+ define_singleton_method :call do |env|
43
+ body = env[RACK_INPUT].read
44
+ return env if body.empty?
45
+
46
+ env[RACK_INPUT].rewind # somebody might try to read this stream
47
+ env[ROUTER_PARAMS] ||= {} # prepare params
48
+
49
+ env[ROUTER_PARAMS].merge!(
50
+ @parsers[
51
+ media_type(env)
52
+ ].parse(body)
53
+ )
54
+
55
+ env
56
+ end
57
+ end
58
+
59
+ def media_type(env)
60
+ if ct = content_type(env)
61
+ ct.split(MEDIA_TYPE_MATCHER, 2).first.downcase
62
+ end
63
+ end
64
+
65
+ def content_type(env)
66
+ content_type = env[CONTENT_TYPE]
67
+ content_type.nil? || content_type.empty? ? nil : content_type
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,28 @@
1
+ require 'json'
2
+
3
+ module Hanami
4
+ module Routing
5
+ module Parsing
6
+ class JsonParser < Parser
7
+ def mime_types
8
+ ['application/json', 'application/vnd.api+json']
9
+ end
10
+
11
+ # Parse a json string
12
+ #
13
+ # @param body [String] a json string
14
+ #
15
+ # @return [Hash] the parsed json
16
+ #
17
+ # @raise [Hanami::Routing::Parsing::BodyParsingError] when the body can't be parsed.
18
+ #
19
+ # @since 0.2.0
20
+ def parse(body)
21
+ JSON.parse(body)
22
+ rescue JSON::ParserError => e
23
+ raise BodyParsingError.new(e.message)
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,58 @@
1
+ require 'hanami/utils/class'
2
+ require 'hanami/utils/string'
3
+ require 'hanami/routing/error'
4
+
5
+ module Hanami
6
+ module Routing
7
+ module Parsing
8
+ # Body parsing error
9
+ # This is raised when parser fails to parse the body
10
+ #
11
+ # @since 0.5.0
12
+ class BodyParsingError < Hanami::Routing::Error
13
+ end
14
+
15
+ # @since 0.2.0
16
+ class UnknownParserError < Hanami::Routing::Error
17
+ def initialize(parser)
18
+ super("Unknown Parser: `#{ parser }'")
19
+ end
20
+ end
21
+
22
+ # @since 0.2.0
23
+ class Parser
24
+ # @since 0.2.0
25
+ def self.for(parser)
26
+ case parser
27
+ when String, Symbol
28
+ require_parser(parser)
29
+ else
30
+ parser
31
+ end
32
+ end
33
+
34
+ # @since 0.2.0
35
+ def mime_types
36
+ raise NotImplementedError
37
+ end
38
+
39
+ # @since 0.2.0
40
+ def parse(body)
41
+ Hash.new
42
+ end
43
+
44
+ private
45
+ # @since 0.2.0
46
+ # @api private
47
+ def self.require_parser(parser)
48
+ require "hanami/routing/parsing/#{ parser }_parser"
49
+
50
+ parser = Utils::String.new(parser).classify
51
+ Utils::Class.load!("Hanami::Routing::Parsing::#{ parser }Parser").new
52
+ rescue LoadError, NameError
53
+ raise UnknownParserError.new(parser)
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,153 @@
1
+ require 'hanami/utils/string'
2
+
3
+ module Hanami
4
+ module Routing
5
+ # Represents a result of router path recognition.
6
+ #
7
+ # @since 0.5.0
8
+ #
9
+ # @see Hanami::Router#recognize
10
+ class RecognizedRoute
11
+ # @since 0.5.0
12
+ # @api private
13
+ REQUEST_METHOD = 'REQUEST_METHOD'.freeze
14
+
15
+ # @since 0.5.0
16
+ # @api private
17
+ NAMESPACE = '%s::'.freeze
18
+
19
+ # @since 0.5.0
20
+ # @api private
21
+ NAMESPACE_REPLACEMENT = ''.freeze
22
+
23
+ # @since 0.5.0
24
+ # @api private
25
+ ACTION_PATH_SEPARATOR = '/'.freeze
26
+
27
+ # @since 0.5.0
28
+ # @api public
29
+ attr_reader :params
30
+
31
+ # Creates a new instance
32
+ #
33
+ # @param response [HttpRouter::Response] raw response of recognition
34
+ # @param env [Hash] Rack env
35
+ # @param router [Hanami::Routing::HttpRouter] low level router
36
+ #
37
+ # @return [Hanami::Routing::RecognizedRoute]
38
+ #
39
+ # @since 0.5.0
40
+ # @api private
41
+ def initialize(response, env, router)
42
+ @env = env
43
+
44
+ unless response.nil?
45
+ @endpoint = response.route.dest
46
+ @params = response.params
47
+ end
48
+
49
+ @namespace = router.namespace
50
+ @action_separator = router.action_separator
51
+ end
52
+
53
+ # Rack protocol compatibility
54
+ #
55
+ # @param env [Hash] Rack env
56
+ #
57
+ # @return [Array] serialized Rack response
58
+ #
59
+ # @raise [Hanami::Router::NotRoutableEndpointError] if not routable
60
+ #
61
+ # @since 0.5.0
62
+ # @api public
63
+ #
64
+ # @see Hanami::Routing::RecognizedRoute#routable?
65
+ # @see Hanami::Router::NotRoutableEndpointError
66
+ def call(env)
67
+ if routable?
68
+ @endpoint.call(env)
69
+ else
70
+ raise Hanami::Router::NotRoutableEndpointError.new(@env)
71
+ end
72
+ end
73
+
74
+ # HTTP verb (aka method)
75
+ #
76
+ # @return [String]
77
+ #
78
+ # @since 0.5.0
79
+ # @api public
80
+ def verb
81
+ @env[REQUEST_METHOD]
82
+ end
83
+
84
+ # Action name
85
+ #
86
+ # @return [String]
87
+ #
88
+ # @since 0.5.0
89
+ # @api public
90
+ #
91
+ # @see Hanami::Router#recognize
92
+ #
93
+ # @example
94
+ # require 'hanami/router'
95
+ #
96
+ # router = Hanami::Router.new do
97
+ # get '/books/:id', to: 'books#show'
98
+ # end
99
+ #
100
+ # puts router.recognize('/books/23').action # => "books#show"
101
+ def action
102
+ namespace = NAMESPACE % @namespace
103
+
104
+ if destination.match(namespace)
105
+ Hanami::Utils::String.new(
106
+ destination.sub(namespace, NAMESPACE_REPLACEMENT)
107
+ ).underscore.rsub(ACTION_PATH_SEPARATOR, @action_separator)
108
+ else
109
+ destination
110
+ end
111
+ end
112
+
113
+ # Check if routable
114
+ #
115
+ # @return [TrueClass,FalseClass]
116
+ #
117
+ # @since 0.5.0
118
+ # @api public
119
+ #
120
+ # @see Hanami::Router#recognize
121
+ #
122
+ # @example
123
+ # require 'hanami/router'
124
+ #
125
+ # router = Hanami::Router.new do
126
+ # get '/', to: 'home#index'
127
+ # end
128
+ #
129
+ # puts router.recognize('/').routable? # => true
130
+ # puts router.recognize('/foo').routable? # => false
131
+ def routable?
132
+ !!@endpoint
133
+ end
134
+
135
+ private
136
+
137
+ # @since 0.5.0
138
+ # @api private
139
+ #
140
+ # @see Hanami::Routing::Endpoint
141
+ def destination
142
+ @destination ||= begin
143
+ case k = @endpoint.__getobj__
144
+ when Class
145
+ k
146
+ else
147
+ k.class
148
+ end.name
149
+ end
150
+ end
151
+ end
152
+ end
153
+ end