hanami-router 0.0.0 → 0.6.0

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,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