hanami-router 1.3.2 → 2.0.0.alpha1
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 +4 -4
- data/CHANGELOG.md +14 -3
- data/README.md +192 -154
- data/hanami-router.gemspec +23 -20
- data/lib/hanami/middleware/body_parser.rb +17 -13
- data/lib/hanami/middleware/body_parser/class_interface.rb +56 -56
- data/lib/hanami/middleware/body_parser/errors.rb +7 -4
- data/lib/hanami/middleware/body_parser/json_parser.rb +5 -3
- data/lib/hanami/middleware/error.rb +16 -0
- data/lib/hanami/router.rb +262 -149
- data/lib/hanami/router/version.rb +3 -1
- data/lib/hanami/routing.rb +193 -0
- data/lib/hanami/routing/endpoint.rb +122 -104
- data/lib/hanami/routing/endpoint_resolver.rb +20 -16
- data/lib/hanami/routing/prefix.rb +102 -0
- data/lib/hanami/routing/recognized_route.rb +40 -26
- data/lib/hanami/routing/resource.rb +9 -7
- data/lib/hanami/routing/resource/action.rb +58 -33
- data/lib/hanami/routing/resource/nested.rb +4 -1
- data/lib/hanami/routing/resource/options.rb +3 -1
- data/lib/hanami/routing/resources.rb +6 -4
- data/lib/hanami/routing/resources/action.rb +11 -6
- data/lib/hanami/routing/routes_inspector.rb +22 -20
- data/lib/hanami/routing/scope.rb +112 -0
- metadata +47 -25
- data/lib/hanami-router.rb +0 -1
- data/lib/hanami/routing/error.rb +0 -7
- data/lib/hanami/routing/force_ssl.rb +0 -212
- data/lib/hanami/routing/http_router.rb +0 -220
- data/lib/hanami/routing/http_router_monkey_patch.rb +0 -38
- data/lib/hanami/routing/namespace.rb +0 -98
- data/lib/hanami/routing/parsers.rb +0 -113
- data/lib/hanami/routing/parsing/json_parser.rb +0 -33
- data/lib/hanami/routing/parsing/parser.rb +0 -61
- data/lib/hanami/routing/route.rb +0 -71
@@ -0,0 +1,193 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "uri"
|
4
|
+
require "rack/utils"
|
5
|
+
require "mustermann/rails"
|
6
|
+
|
7
|
+
# Hanami
|
8
|
+
#
|
9
|
+
# @since 0.1.0
|
10
|
+
module Hanami
|
11
|
+
# Hanami routing
|
12
|
+
#
|
13
|
+
# @since 0.1.0
|
14
|
+
module Routing
|
15
|
+
PATH_INFO = "PATH_INFO"
|
16
|
+
QUERY_STRING = "QUERY_STRING"
|
17
|
+
REQUEST_METHOD = "REQUEST_METHOD"
|
18
|
+
|
19
|
+
HTTP_VERBS = %w[get post delete put patch trace options link unlink].freeze
|
20
|
+
|
21
|
+
PARAMS = "router.params"
|
22
|
+
|
23
|
+
def self.http_verbs
|
24
|
+
HTTP_VERBS
|
25
|
+
end
|
26
|
+
|
27
|
+
# @since 0.5.0
|
28
|
+
class Error < ::StandardError
|
29
|
+
end
|
30
|
+
|
31
|
+
# Invalid route
|
32
|
+
# This is raised when the router fails to recognize a route, because of the
|
33
|
+
# given arguments.
|
34
|
+
#
|
35
|
+
# @since 0.1.0
|
36
|
+
class InvalidRouteException < Error
|
37
|
+
end
|
38
|
+
|
39
|
+
# Endpoint not found
|
40
|
+
# This is raised when the router fails to load an endpoint at the runtime.
|
41
|
+
#
|
42
|
+
# @since 0.1.0
|
43
|
+
class EndpointNotFound < Error
|
44
|
+
end
|
45
|
+
|
46
|
+
# @since 2.0.0
|
47
|
+
class NotCallableEndpointError < Error
|
48
|
+
def initialize(endpoint)
|
49
|
+
super("#{endpoint.inspect} isn't compatible with Rack. Please make sure it implements #call.")
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# HTTP redirect
|
54
|
+
#
|
55
|
+
# @since 2.0.0
|
56
|
+
# @api private
|
57
|
+
class Redirect
|
58
|
+
# @since 2.0.0
|
59
|
+
# @api private
|
60
|
+
LOCATION = "Location"
|
61
|
+
|
62
|
+
# @since 2.0.0
|
63
|
+
# @api private
|
64
|
+
STATUS_RANGE = (300..399).freeze
|
65
|
+
|
66
|
+
attr_reader :path
|
67
|
+
alias destination_path path
|
68
|
+
|
69
|
+
# Instantiate a new redirect
|
70
|
+
#
|
71
|
+
# @param path [String] a relative or absolute URI
|
72
|
+
# @param status [Integer] a redirect status (an integer between `300` and `399`)
|
73
|
+
#
|
74
|
+
# @return [Hanami::Routing::Redirect] a new instance
|
75
|
+
#
|
76
|
+
# @raise [ArgumentError] if path is nil, or status code isn't a redirect
|
77
|
+
#
|
78
|
+
# @since 2.0.0
|
79
|
+
# @api private
|
80
|
+
def initialize(path, status)
|
81
|
+
raise ArgumentError.new("Path is nil") if path.nil?
|
82
|
+
raise ArgumentError.new("Status code isn't a redirect: #{status.inspect}") unless STATUS_RANGE.include?(status)
|
83
|
+
|
84
|
+
@path = path
|
85
|
+
@status = status
|
86
|
+
freeze
|
87
|
+
end
|
88
|
+
|
89
|
+
def call(*)
|
90
|
+
[@status, { LOCATION => @path }, []]
|
91
|
+
end
|
92
|
+
|
93
|
+
def redirect?
|
94
|
+
true
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# Route
|
99
|
+
#
|
100
|
+
# @since 0.1.0
|
101
|
+
# @api private
|
102
|
+
class Route
|
103
|
+
# @since 0.7.0
|
104
|
+
# @api private
|
105
|
+
def initialize(verb, path, endpoint, constraints)
|
106
|
+
@verb = verb
|
107
|
+
@path = Mustermann.new(path, type: :rails, version: "5.0", capture: constraints)
|
108
|
+
@endpoint = endpoint
|
109
|
+
freeze
|
110
|
+
end
|
111
|
+
|
112
|
+
# @since 0.1.0
|
113
|
+
# @api private
|
114
|
+
def call(env)
|
115
|
+
env[PARAMS] ||= {}
|
116
|
+
env[PARAMS].merge!(Rack::Utils.parse_nested_query(env[QUERY_STRING]))
|
117
|
+
env[PARAMS].merge!(@path.params(env[PATH_INFO]))
|
118
|
+
env[PARAMS] = Utils::Hash.deep_symbolize(env[PARAMS])
|
119
|
+
|
120
|
+
@endpoint.call(env)
|
121
|
+
end
|
122
|
+
|
123
|
+
# @since 0.1.0
|
124
|
+
# @api private
|
125
|
+
def path(args)
|
126
|
+
@path.expand(:append, args)
|
127
|
+
rescue Mustermann::ExpandError => e
|
128
|
+
raise Hanami::Routing::InvalidRouteException.new(e.message)
|
129
|
+
end
|
130
|
+
|
131
|
+
# @since 2.0.0
|
132
|
+
# @api private
|
133
|
+
def match?(env)
|
134
|
+
match_path?(env) &&
|
135
|
+
@verb.include?(env[REQUEST_METHOD])
|
136
|
+
end
|
137
|
+
|
138
|
+
# @since 2.0.0
|
139
|
+
# @api private
|
140
|
+
def match_path?(env)
|
141
|
+
@path =~ env[PATH_INFO]
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
# @since 2.0.0
|
146
|
+
# @api private
|
147
|
+
module Uri
|
148
|
+
# @since 2.0.0
|
149
|
+
# @api private
|
150
|
+
HTTP = "http"
|
151
|
+
|
152
|
+
# @since 2.0.0
|
153
|
+
# @api private
|
154
|
+
HTTPS = "https"
|
155
|
+
|
156
|
+
# @since 2.0.0
|
157
|
+
# @api private
|
158
|
+
DEFAULT_SCHEME = HTTP
|
159
|
+
|
160
|
+
# Build a URI string from the given arguments
|
161
|
+
#
|
162
|
+
# @param scheme [String] the URI scheme: one of `"http"` or `"https"`
|
163
|
+
# @param host [String] the URI host
|
164
|
+
# @param port [String,Integer] the URI port
|
165
|
+
#
|
166
|
+
# @raise [ArgumentError] if one of `scheme`, `host`, `port` is `nil`, or
|
167
|
+
# if `scheme` is unknown
|
168
|
+
#
|
169
|
+
# @since 2.0.0
|
170
|
+
# @api private
|
171
|
+
def self.build(scheme:, host:, port:)
|
172
|
+
raise ArgumentError.new("host is nil") if host.nil?
|
173
|
+
raise ArgumentError.new("port is nil") if port.nil?
|
174
|
+
|
175
|
+
case scheme
|
176
|
+
when HTTP
|
177
|
+
URI::HTTP
|
178
|
+
when HTTPS
|
179
|
+
URI::HTTPS
|
180
|
+
else
|
181
|
+
raise ArgumentError.new("Unknown scheme: #{scheme.inspect}")
|
182
|
+
end.build(scheme: scheme, host: host, port: port).to_s
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
require "hanami/routing/endpoint"
|
187
|
+
require "hanami/routing/prefix"
|
188
|
+
require "hanami/routing/scope"
|
189
|
+
require "hanami/routing/resource"
|
190
|
+
require "hanami/routing/resources"
|
191
|
+
require "hanami/routing/recognized_route"
|
192
|
+
end
|
193
|
+
end
|
@@ -1,57 +1,77 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
require
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "delegate"
|
4
|
+
require "hanami/utils/class"
|
5
|
+
require "hanami/utils/string"
|
4
6
|
|
5
7
|
module Hanami
|
6
8
|
module Routing
|
7
|
-
#
|
8
|
-
# This is raised when the router fails to load an endpoint at the runtime.
|
9
|
-
#
|
10
|
-
# @since 0.1.0
|
11
|
-
class EndpointNotFound < Hanami::Routing::Error
|
12
|
-
end
|
13
|
-
|
14
|
-
# Routing endpoint
|
15
|
-
# This is the object that responds to an HTTP request made against a certain
|
16
|
-
# path.
|
17
|
-
#
|
18
|
-
# The router will use this class for:
|
19
|
-
#
|
20
|
-
# * Procs and any Rack compatible object (respond to #call)
|
21
|
-
#
|
22
|
-
# @since 0.1.0
|
9
|
+
# Routes endpoint
|
23
10
|
#
|
11
|
+
# @since 2.0.0
|
24
12
|
# @api private
|
25
|
-
|
26
|
-
|
27
|
-
# require 'hanami/router'
|
28
|
-
#
|
29
|
-
# Hanami::Router.new do
|
30
|
-
# get '/proc', to: ->(env) { [200, {}, ['This will use Hanami::Routing::Endpoint']] }
|
31
|
-
# get '/rack-app', to: RackApp.new
|
32
|
-
# end
|
33
|
-
class Endpoint < SimpleDelegator
|
34
|
-
# @since 0.2.0
|
13
|
+
module Endpoint
|
14
|
+
# @since 2.0.0
|
35
15
|
# @api private
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
source, line = __getobj__.source_location
|
40
|
-
lambda_inspector = " (lambda)" if __getobj__.lambda?
|
41
|
-
|
42
|
-
"#<Proc@#{ ::File.expand_path(source) }:#{ line }#{ lambda_inspector }>"
|
43
|
-
when Class
|
44
|
-
__getobj__
|
45
|
-
else
|
46
|
-
"#<#{ __getobj__.class }>"
|
47
|
-
end
|
48
|
-
end
|
16
|
+
#
|
17
|
+
# FIXME: Shall this be the default of Utils::Class.load! ?
|
18
|
+
DEFAULT_NAMESPACE = Object
|
49
19
|
|
50
|
-
#
|
20
|
+
# Controller / action separator for Hanami
|
21
|
+
#
|
22
|
+
# @since 2.0.0
|
51
23
|
# @api private
|
52
|
-
|
53
|
-
|
54
|
-
|
24
|
+
#
|
25
|
+
# @example
|
26
|
+
# require "hanami/router"
|
27
|
+
#
|
28
|
+
# Hanami::Router.new do
|
29
|
+
# get "/home", to: "home#index"
|
30
|
+
# end
|
31
|
+
ACTION_SEPARATOR = "#"
|
32
|
+
|
33
|
+
# Replacement to load an action from the string name.
|
34
|
+
#
|
35
|
+
# Please note that the `"/"` value is required by `Hanami::Utils::String#classify`.
|
36
|
+
#
|
37
|
+
# Given the `"home#index"` string, with the `Web::Controllers` namespace,
|
38
|
+
# it will try to load `Web::Controllers::Home::Index` action.
|
39
|
+
#
|
40
|
+
# @since 2.0.0
|
41
|
+
# @api private
|
42
|
+
ACTION_SEPARATOR_REPLACEMENT = "/"
|
43
|
+
|
44
|
+
# Find an endpoint for the given name
|
45
|
+
#
|
46
|
+
# @param name [String,Class,Proc,Object] the endpoint expressed as name
|
47
|
+
# (`String`), as a Rack class application (`Class`), as a Rack
|
48
|
+
# compatible proc (`Proc`), or as any other Rack compatible object
|
49
|
+
# (`Object`)
|
50
|
+
# @param namespace [Module] the Ruby module where to lookup the endpoint
|
51
|
+
# @param configuration [Hanami::Controller::Configuration] the action
|
52
|
+
# configuration
|
53
|
+
#
|
54
|
+
# @raise [Hanami::Routing::NotCallableEndpointError] if the found object
|
55
|
+
# doesn't implement Rack protocol (`#call`)
|
56
|
+
#
|
57
|
+
# @return [Object, Hanami::Routing::LazyEndpoint] a Rack compatible
|
58
|
+
# endpoint
|
59
|
+
#
|
60
|
+
# @since 2.0.0
|
61
|
+
# @api private
|
62
|
+
def self.find(name, namespace, configuration = nil)
|
63
|
+
endpoint = case name
|
64
|
+
when String
|
65
|
+
find_string(name, namespace || DEFAULT_NAMESPACE, configuration)
|
66
|
+
when Class
|
67
|
+
name.respond_to?(:call) ? name : name.new
|
68
|
+
else
|
69
|
+
name
|
70
|
+
end
|
71
|
+
|
72
|
+
raise NotCallableEndpointError.new(endpoint) unless endpoint.respond_to?(:call)
|
73
|
+
|
74
|
+
endpoint
|
55
75
|
end
|
56
76
|
|
57
77
|
# @since 1.0.1
|
@@ -64,42 +84,47 @@ module Hanami
|
|
64
84
|
# @api private
|
65
85
|
def destination_path
|
66
86
|
end
|
67
|
-
end
|
68
87
|
|
69
|
-
|
70
|
-
# This is the object that responds to an HTTP request made against a certain
|
71
|
-
# path.
|
72
|
-
#
|
73
|
-
# The router will use this class for:
|
74
|
-
#
|
75
|
-
# * Classes
|
76
|
-
# * Hanami::Action endpoints referenced as a class
|
77
|
-
# * Hanami::Action endpoints referenced a string
|
78
|
-
# * RESTful resource(s)
|
79
|
-
#
|
80
|
-
# @since 0.1.0
|
81
|
-
#
|
82
|
-
# @api private
|
83
|
-
#
|
84
|
-
# @example
|
85
|
-
# require 'hanami/router'
|
86
|
-
#
|
87
|
-
# Hanami::Router.new do
|
88
|
-
# get '/class', to: RackMiddleware
|
89
|
-
# get '/hanami-action-class', to: Dashboard::Index
|
90
|
-
# get '/hanami-action-string', to: 'dashboard#index'
|
91
|
-
#
|
92
|
-
# resource 'identity'
|
93
|
-
# resources 'articles'
|
94
|
-
# end
|
95
|
-
class ClassEndpoint < Endpoint
|
96
|
-
# Rack interface
|
88
|
+
# Find an endpoint from its name
|
97
89
|
#
|
98
|
-
# @
|
90
|
+
# @param name [String] the endpoint name
|
91
|
+
# @param namespace [Module] the Ruby module where to lookup the endpoint
|
92
|
+
# @param configuration [Hanami::Controller::Configuration] the action
|
93
|
+
# configuration
|
94
|
+
#
|
95
|
+
# @return [Object, Hanami::Routing::LazyEndpoint] a Rack compatible
|
96
|
+
# endpoint
|
97
|
+
#
|
98
|
+
# @since 2.0.0
|
99
99
|
# @api private
|
100
|
-
|
101
|
-
|
100
|
+
#
|
101
|
+
# @example Basic Usage
|
102
|
+
# Hanami::Routing::Endpoint.find("MyMiddleware")
|
103
|
+
# # => #<MyMiddleware:0x007ff6df06f468>
|
104
|
+
#
|
105
|
+
# @example Hanami Action
|
106
|
+
# Hanami::Routing::Endpoint.find("home#index", Web::Controllers)
|
107
|
+
# # => #<Web::Controllers::Home::Index:0x007ff6df06f468>
|
108
|
+
def self.find_string(name, namespace, configuration)
|
109
|
+
n = Utils::String.new(name.sub(ACTION_SEPARATOR, ACTION_SEPARATOR_REPLACEMENT)).classify.to_s
|
110
|
+
klass = Utils::Class.load!(n, namespace)
|
111
|
+
|
112
|
+
if hanami_action?(name, n)
|
113
|
+
klass.new(configuration: configuration)
|
114
|
+
else
|
115
|
+
klass.new
|
116
|
+
end
|
117
|
+
rescue NameError
|
118
|
+
Hanami::Routing::LazyEndpoint.new(n, namespace)
|
102
119
|
end
|
120
|
+
|
121
|
+
private_class_method :find_string
|
122
|
+
|
123
|
+
def self.hanami_action?(name, endpoint)
|
124
|
+
name != endpoint
|
125
|
+
end
|
126
|
+
|
127
|
+
private_class_method :hanami_action?
|
103
128
|
end
|
104
129
|
|
105
130
|
# Routing endpoint
|
@@ -121,13 +146,14 @@ module Hanami
|
|
121
146
|
# @api private
|
122
147
|
#
|
123
148
|
# @see Hanami::Routing::ClassEndpoint
|
124
|
-
class LazyEndpoint <
|
149
|
+
class LazyEndpoint < SimpleDelegator
|
125
150
|
# Initialize the lazy endpoint
|
126
151
|
#
|
127
152
|
# @since 0.1.0
|
128
153
|
# @api private
|
129
154
|
def initialize(name, namespace)
|
130
|
-
@name
|
155
|
+
@name = name
|
156
|
+
@namespace = namespace
|
131
157
|
end
|
132
158
|
|
133
159
|
# Rack interface
|
@@ -143,19 +169,32 @@ module Hanami
|
|
143
169
|
# @since 0.2.0
|
144
170
|
# @api private
|
145
171
|
def inspect
|
146
|
-
# TODO review this implementation once the namespace feature will be
|
172
|
+
# TODO: review this implementation once the namespace feature will be
|
147
173
|
# cleaned up.
|
148
|
-
result =
|
174
|
+
result = begin
|
175
|
+
klass
|
176
|
+
rescue
|
177
|
+
nil
|
178
|
+
end
|
149
179
|
|
150
180
|
if result.nil?
|
151
181
|
result = @name
|
152
|
-
result = "#{
|
182
|
+
result = "#{@namespace}::#{result}" if @namespace != Object
|
153
183
|
end
|
154
184
|
|
155
185
|
result
|
156
186
|
end
|
157
187
|
|
188
|
+
# @since 1.0.0
|
189
|
+
# @api private
|
190
|
+
def routable?
|
191
|
+
!__getobj__.nil?
|
192
|
+
rescue ArgumentError
|
193
|
+
false
|
194
|
+
end
|
195
|
+
|
158
196
|
private
|
197
|
+
|
159
198
|
# @since 0.1.0
|
160
199
|
# @api private
|
161
200
|
def obj
|
@@ -170,26 +209,5 @@ module Hanami
|
|
170
209
|
raise EndpointNotFound.new(e.message)
|
171
210
|
end
|
172
211
|
end
|
173
|
-
|
174
|
-
# @since 1.0.1
|
175
|
-
# @api private
|
176
|
-
class RedirectEndpoint < Endpoint
|
177
|
-
# @since 1.0.1
|
178
|
-
# @api private
|
179
|
-
attr_reader :destination_path
|
180
|
-
|
181
|
-
# @since 1.0.1
|
182
|
-
# @api private
|
183
|
-
def initialize(destination_path, destination)
|
184
|
-
@destination_path = destination_path
|
185
|
-
super(destination)
|
186
|
-
end
|
187
|
-
|
188
|
-
# @since 1.0.1
|
189
|
-
# @api private
|
190
|
-
def redirect?
|
191
|
-
true
|
192
|
-
end
|
193
|
-
end
|
194
212
|
end
|
195
213
|
end
|