lotus-router 0.0.0 → 0.1.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.
- checksums.yaml +4 -4
- data/.coveralls.yml +2 -0
- data/.gitignore +5 -13
- data/.travis.yml +5 -0
- data/.yardopts +3 -0
- data/Gemfile +10 -3
- data/README.md +470 -6
- data/Rakefile +10 -1
- data/benchmarks/callable +23 -0
- data/benchmarks/named_routes +72 -0
- data/benchmarks/resource +44 -0
- data/benchmarks/resources +58 -0
- data/benchmarks/routes +67 -0
- data/benchmarks/run.sh +11 -0
- data/benchmarks/utils.rb +56 -0
- data/lib/lotus-router.rb +1 -0
- data/lib/lotus/router.rb +752 -3
- data/lib/lotus/router/version.rb +2 -2
- data/lib/lotus/routing/endpoint.rb +114 -0
- data/lib/lotus/routing/endpoint_resolver.rb +251 -0
- data/lib/lotus/routing/http_router.rb +130 -0
- data/lib/lotus/routing/namespace.rb +86 -0
- data/lib/lotus/routing/resource.rb +73 -0
- data/lib/lotus/routing/resource/action.rb +340 -0
- data/lib/lotus/routing/resource/options.rb +48 -0
- data/lib/lotus/routing/resources.rb +40 -0
- data/lib/lotus/routing/resources/action.rb +123 -0
- data/lib/lotus/routing/route.rb +53 -0
- data/lotus-router.gemspec +16 -12
- data/test/fixtures.rb +193 -0
- data/test/integration/client_error_test.rb +16 -0
- data/test/integration/pass_on_response_test.rb +13 -0
- data/test/named_routes_test.rb +123 -0
- data/test/namespace_test.rb +289 -0
- data/test/new_test.rb +67 -0
- data/test/redirect_test.rb +33 -0
- data/test/resource_test.rb +128 -0
- data/test/resources_test.rb +136 -0
- data/test/routing/endpoint_resolver_test.rb +110 -0
- data/test/routing/resource/options_test.rb +36 -0
- data/test/routing_test.rb +99 -0
- data/test/test_helper.rb +32 -0
- data/test/version_test.rb +7 -0
- metadata +102 -10
data/lib/lotus/router/version.rb
CHANGED
@@ -0,0 +1,114 @@
|
|
1
|
+
require 'delegate'
|
2
|
+
require 'lotus/utils/class'
|
3
|
+
|
4
|
+
module Lotus
|
5
|
+
module Routing
|
6
|
+
# Endpoint not found
|
7
|
+
# This is raised when the router fails to load an endpoint at the runtime.
|
8
|
+
#
|
9
|
+
# @since 0.1.0
|
10
|
+
class EndpointNotFound < ::Exception
|
11
|
+
end
|
12
|
+
|
13
|
+
# Routing endpoint
|
14
|
+
# This is the object that responds to an HTTP request made against a certain
|
15
|
+
# path.
|
16
|
+
#
|
17
|
+
# The router will use this class for:
|
18
|
+
#
|
19
|
+
# * Procs and any Rack compatible object (respond to #call)
|
20
|
+
#
|
21
|
+
# @since 0.1.0
|
22
|
+
#
|
23
|
+
# @api private
|
24
|
+
#
|
25
|
+
# @example
|
26
|
+
# require 'lotus/router'
|
27
|
+
#
|
28
|
+
# Lotus::Router.new do
|
29
|
+
# get '/proc', to: ->(env) { [200, {}, ['This will use Lotus::Routing::Endpoint']] }
|
30
|
+
# get '/rack-app', to: RackApp.new
|
31
|
+
# end
|
32
|
+
class Endpoint < SimpleDelegator
|
33
|
+
end
|
34
|
+
|
35
|
+
# Routing endpoint
|
36
|
+
# This is the object that responds to an HTTP request made against a certain
|
37
|
+
# path.
|
38
|
+
#
|
39
|
+
# The router will use this class for:
|
40
|
+
#
|
41
|
+
# * Classes
|
42
|
+
# * Lotus::Action endpoints referenced as a class
|
43
|
+
# * Lotus::Action endpoints referenced a string
|
44
|
+
# * RESTful resource(s)
|
45
|
+
#
|
46
|
+
# @since 0.1.0
|
47
|
+
#
|
48
|
+
# @api private
|
49
|
+
#
|
50
|
+
# @example
|
51
|
+
# require 'lotus/router'
|
52
|
+
#
|
53
|
+
# Lotus::Router.new do
|
54
|
+
# get '/class', to: RackMiddleware
|
55
|
+
# get '/lotus-action-class', to: DashboardController::Index
|
56
|
+
# get '/lotus-action-string', to: 'dashboard#index'
|
57
|
+
#
|
58
|
+
# resource 'identity'
|
59
|
+
# resources 'articles'
|
60
|
+
# end
|
61
|
+
class ClassEndpoint < Endpoint
|
62
|
+
# Rack interface
|
63
|
+
#
|
64
|
+
# @since 0.1.0
|
65
|
+
def call(env)
|
66
|
+
__getobj__.new.call(env)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Routing endpoint
|
71
|
+
# This is the object that responds to an HTTP request made against a certain
|
72
|
+
# path.
|
73
|
+
#
|
74
|
+
# The router will use this class for the same use cases of `ClassEndpoint`,
|
75
|
+
# but when the target class can't be found, instead of raise a `LoadError`
|
76
|
+
# we reference in a lazy endpoint.
|
77
|
+
#
|
78
|
+
# For each incoming HTTP request, it will look for the referenced class,
|
79
|
+
# then it will instantiate and invoke #call on the object.
|
80
|
+
#
|
81
|
+
# This behavior is required to solve a chicken-egg situation when we try
|
82
|
+
# to load the router first and then the application with all its endpoints.
|
83
|
+
#
|
84
|
+
# @since 0.1.0
|
85
|
+
#
|
86
|
+
# @api private
|
87
|
+
#
|
88
|
+
# @see Lotus::Routing::ClassEndpoint
|
89
|
+
class LazyEndpoint < Endpoint
|
90
|
+
# Initialize the lazy endpoint
|
91
|
+
#
|
92
|
+
# @since 0.1.0
|
93
|
+
def initialize(name, namespace)
|
94
|
+
@name, @namespace = name, namespace
|
95
|
+
end
|
96
|
+
|
97
|
+
# Rack interface
|
98
|
+
#
|
99
|
+
# @raise [EndpointNotFound] when the endpoint can't be found.
|
100
|
+
#
|
101
|
+
# @since 0.1.0
|
102
|
+
def call(env)
|
103
|
+
obj.call(env)
|
104
|
+
end
|
105
|
+
|
106
|
+
private
|
107
|
+
def obj
|
108
|
+
Utils::Class.load!(@name, @namespace).new
|
109
|
+
rescue NameError => e
|
110
|
+
raise EndpointNotFound.new(e.message)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
@@ -0,0 +1,251 @@
|
|
1
|
+
require 'lotus/utils/string'
|
2
|
+
require 'lotus/utils/class'
|
3
|
+
require 'lotus/routing/endpoint'
|
4
|
+
|
5
|
+
module Lotus
|
6
|
+
module Routing
|
7
|
+
# Resolve duck-typed endpoints
|
8
|
+
#
|
9
|
+
# @since 0.1.0
|
10
|
+
#
|
11
|
+
# @api private
|
12
|
+
class EndpointResolver
|
13
|
+
# Default suffix appended to the controller#action string.
|
14
|
+
# A different suffix can be passed to #initialize with the `:suffix` option.
|
15
|
+
#
|
16
|
+
# @see #initialize
|
17
|
+
# @see #resolve
|
18
|
+
#
|
19
|
+
# @since 0.1.0
|
20
|
+
#
|
21
|
+
# @example
|
22
|
+
# require 'lotus/router'
|
23
|
+
#
|
24
|
+
# router = Lotus::Router.new do
|
25
|
+
# get '/', to: 'articles#show'
|
26
|
+
# end
|
27
|
+
#
|
28
|
+
# # That string is transformed into "Articles(::Controller::|Controller::)Show"
|
29
|
+
# # because the risolver is able to lookup (in the given order) for:
|
30
|
+
# #
|
31
|
+
# # * Articles::Controller::Show
|
32
|
+
# # * ArticlesController::Show
|
33
|
+
# #
|
34
|
+
# # When it finds a class, it stops the lookup and returns the result.
|
35
|
+
SUFFIX = '(::Controller::|Controller::)'.freeze
|
36
|
+
|
37
|
+
# Default separator for controller and action.
|
38
|
+
# A different separator can be passed to #initialize with the `:separator` option.
|
39
|
+
#
|
40
|
+
# @see #initialize
|
41
|
+
# @see #resolve
|
42
|
+
#
|
43
|
+
# @since 0.1.0
|
44
|
+
#
|
45
|
+
# @example
|
46
|
+
# require 'lotus/router'
|
47
|
+
#
|
48
|
+
# router = Lotus::Router.new do
|
49
|
+
# get '/', to: 'articles#show'
|
50
|
+
# end
|
51
|
+
ACTION_SEPARATOR = '#'.freeze
|
52
|
+
|
53
|
+
attr_reader :action_separator
|
54
|
+
|
55
|
+
# Initialize an endpoint resolver
|
56
|
+
#
|
57
|
+
# @param options [Hash] the options used to customize lookup behavior
|
58
|
+
#
|
59
|
+
# @option options [Class] :endpoint the endpoint class that is returned
|
60
|
+
# by `#resolve`. (defaults to `Lotus::Routing::Endpoint`)
|
61
|
+
#
|
62
|
+
# @option options [Class,Module] :namespace the Ruby namespace where to
|
63
|
+
# lookup for controllers and actions. (defaults to `Object`)
|
64
|
+
#
|
65
|
+
# @option options [String] :suffix the suffix appended to the controller
|
66
|
+
# name during the lookup. (defaults to `SUFFIX`)
|
67
|
+
#
|
68
|
+
# @option options [String] :action_separator the sepatator between controller and
|
69
|
+
# action name. (defaults to `ACTION_SEPARATOR`)
|
70
|
+
#
|
71
|
+
#
|
72
|
+
#
|
73
|
+
# @return [Lotus::Routing::EndpointResolver] self
|
74
|
+
#
|
75
|
+
#
|
76
|
+
#
|
77
|
+
# @since 0.1.0
|
78
|
+
#
|
79
|
+
#
|
80
|
+
#
|
81
|
+
# @example Specify custom endpoint class
|
82
|
+
# require 'lotus/router'
|
83
|
+
#
|
84
|
+
# resolver = Lotus::Routing::EndpointResolver.new(endpoint: CustomEndpoint)
|
85
|
+
# router = Lotus::Router.new(resolver: resolver)
|
86
|
+
#
|
87
|
+
# router.get('/', to: endpoint).dest # => #<CustomEndpoint:0x007f97f3359570 ...>
|
88
|
+
#
|
89
|
+
#
|
90
|
+
#
|
91
|
+
# @example Specify custom Ruby namespace
|
92
|
+
# require 'lotus/router'
|
93
|
+
#
|
94
|
+
# resolver = Lotus::Routing::EndpointResolver.new(namespace: MyApp)
|
95
|
+
# router = Lotus::Router.new(resolver: resolver)
|
96
|
+
#
|
97
|
+
# router.get('/', to: 'articles#show')
|
98
|
+
# # => Will look for:
|
99
|
+
# # * MyApp::Articles::Controller::Show
|
100
|
+
# # * MyApp::ArticlesController::Show
|
101
|
+
#
|
102
|
+
#
|
103
|
+
#
|
104
|
+
# @example Specify custom controller suffix
|
105
|
+
# require 'lotus/router'
|
106
|
+
#
|
107
|
+
# resolver = Lotus::Routing::EndpointResolver.new(suffix: '(Controller::|Ctrl::)')
|
108
|
+
# router = Lotus::Router.new(resolver: resolver)
|
109
|
+
#
|
110
|
+
# router.get('/', to: 'articles#show')
|
111
|
+
# # => Will look for:
|
112
|
+
# # * ArticlesController::Show
|
113
|
+
# # * ArticlesCtrl::Show
|
114
|
+
#
|
115
|
+
#
|
116
|
+
#
|
117
|
+
# @example Specify custom controller-action separator
|
118
|
+
# require 'lotus/router'
|
119
|
+
#
|
120
|
+
# resolver = Lotus::Routing::EndpointResolver.new(separator: '@')
|
121
|
+
# router = Lotus::Router.new(resolver: resolver)
|
122
|
+
#
|
123
|
+
# router.get('/', to: 'articles@show')
|
124
|
+
# # => Will look for:
|
125
|
+
# # * Articles::Controller::Show
|
126
|
+
# # * ArticlesController::Show
|
127
|
+
def initialize(options = {})
|
128
|
+
@endpoint_class = options[:endpoint] || Endpoint
|
129
|
+
@namespace = options[:namespace] || Object
|
130
|
+
@suffix = options[:suffix] || SUFFIX
|
131
|
+
@action_separator = options[:action_separator] || ACTION_SEPARATOR
|
132
|
+
end
|
133
|
+
|
134
|
+
# Resolve the given set of HTTP verb, path, endpoint and options.
|
135
|
+
# If it fails to resolve, it will mount the default endpoint to the given
|
136
|
+
# path, which returns an 404 (Not Found).
|
137
|
+
#
|
138
|
+
# @param options [Hash] the options required to resolve the endpoint
|
139
|
+
#
|
140
|
+
# @option options [String,Proc,Class,Object#call] :to the endpoint
|
141
|
+
# @option options [String] :prefix an optional path prefix
|
142
|
+
#
|
143
|
+
# @return [Endpoint] this may vary according to the :endpoint option
|
144
|
+
# passed to #initialize
|
145
|
+
#
|
146
|
+
# @since 0.1.0
|
147
|
+
#
|
148
|
+
# @see #initialize
|
149
|
+
# @see #find
|
150
|
+
#
|
151
|
+
# @example Resolve to a Proc
|
152
|
+
# require 'lotus/router'
|
153
|
+
#
|
154
|
+
# router = Lotus::Router.new
|
155
|
+
# router.get '/', to: ->(env) { [200, {}, ['Hi!']] }
|
156
|
+
#
|
157
|
+
# @example Resolve to a class
|
158
|
+
# require 'lotus/router'
|
159
|
+
#
|
160
|
+
# router = Lotus::Router.new
|
161
|
+
# router.get '/', to: RackMiddleware
|
162
|
+
#
|
163
|
+
# @example Resolve to a Rack compatible object (respond to #call)
|
164
|
+
# require 'lotus/router'
|
165
|
+
#
|
166
|
+
# router = Lotus::Router.new
|
167
|
+
# router.get '/', to: AnotherMiddleware.new
|
168
|
+
#
|
169
|
+
# @example Resolve to a Lotus::Action from a string (see Lotus::Controller framework)
|
170
|
+
# require 'lotus/router'
|
171
|
+
#
|
172
|
+
# router = Lotus::Router.new
|
173
|
+
# router.get '/', to: 'articles#show'
|
174
|
+
#
|
175
|
+
# @example Resolve to a Lotus::Action (see Lotus::Controller framework)
|
176
|
+
# require 'lotus/router'
|
177
|
+
#
|
178
|
+
# router = Lotus::Router.new
|
179
|
+
# router.get '/', to: ArticlesController::Show
|
180
|
+
#
|
181
|
+
# @example Resolve with a path prefix
|
182
|
+
# require 'lotus/router'
|
183
|
+
#
|
184
|
+
# router = Lotus::Router.new
|
185
|
+
# router.get '/dashboard', to: BackendApp.new, prefix: 'backend'
|
186
|
+
# # => Will be available under '/backend/dashboard'
|
187
|
+
def resolve(options, &endpoint)
|
188
|
+
result = endpoint || find(options)
|
189
|
+
resolve_callable(result) || resolve_matchable(result) || default
|
190
|
+
end
|
191
|
+
|
192
|
+
# Finds a path from the given options.
|
193
|
+
#
|
194
|
+
# @param options [Hash] the path description
|
195
|
+
# @option options [String,Proc,Class,Object#call] :to the endpoint
|
196
|
+
# @option options [String] :prefix an optional path prefix
|
197
|
+
#
|
198
|
+
# @since 0.1.0
|
199
|
+
#
|
200
|
+
# @return [Object]
|
201
|
+
def find(options)
|
202
|
+
if prefix = options[:prefix]
|
203
|
+
prefix.join(options[:to])
|
204
|
+
else
|
205
|
+
options[:to]
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
protected
|
210
|
+
def default
|
211
|
+
@endpoint_class.new(
|
212
|
+
->(env) { [404, {'X-Cascade' => 'pass'}, 'Not Found'] }
|
213
|
+
)
|
214
|
+
end
|
215
|
+
|
216
|
+
def constantize(string)
|
217
|
+
begin
|
218
|
+
ClassEndpoint.new(Utils::Class.load!(string, @namespace))
|
219
|
+
rescue NameError
|
220
|
+
LazyEndpoint.new(string, @namespace)
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
def classify(string)
|
225
|
+
Utils::String.new(string).classify
|
226
|
+
end
|
227
|
+
|
228
|
+
private
|
229
|
+
def resolve_callable(callable)
|
230
|
+
if callable.respond_to?(:call)
|
231
|
+
@endpoint_class.new(callable)
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
def resolve_matchable(matchable)
|
236
|
+
if matchable.respond_to?(:match)
|
237
|
+
constantize(
|
238
|
+
resolve_action(matchable) || classify(matchable)
|
239
|
+
)
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
def resolve_action(string)
|
244
|
+
if string.match(action_separator)
|
245
|
+
controller, action = string.split(action_separator).map {|token| classify(token) }
|
246
|
+
controller + @suffix + action
|
247
|
+
end
|
248
|
+
end
|
249
|
+
end
|
250
|
+
end
|
251
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
require 'http_router'
|
2
|
+
require 'lotus/utils/io'
|
3
|
+
require 'lotus/routing/endpoint_resolver'
|
4
|
+
require 'lotus/routing/route'
|
5
|
+
|
6
|
+
Lotus::Utils::IO.silence_warnings do
|
7
|
+
HttpRouter::Route::VALID_HTTP_VERBS = %w{GET POST PUT PATCH DELETE HEAD OPTIONS TRACE}
|
8
|
+
end
|
9
|
+
|
10
|
+
module Lotus
|
11
|
+
module Routing
|
12
|
+
# Invalid route
|
13
|
+
# This is raised when the router fails to recognize a route, because of the
|
14
|
+
# given arguments.
|
15
|
+
#
|
16
|
+
# @since 0.1.0
|
17
|
+
class InvalidRouteException < ::Exception
|
18
|
+
end
|
19
|
+
|
20
|
+
# HTTP router
|
21
|
+
#
|
22
|
+
# This implementation is based on ::HttpRouter (http_router gem).
|
23
|
+
#
|
24
|
+
# Lotus::Router wraps an instance of this class, in order to protect its
|
25
|
+
# public API from any future change of ::HttpRouter.
|
26
|
+
#
|
27
|
+
# @since 0.1.0
|
28
|
+
# @api private
|
29
|
+
class HttpRouter < ::HttpRouter
|
30
|
+
# Initialize the router.
|
31
|
+
#
|
32
|
+
# @see Lotus::Router#initialize
|
33
|
+
#
|
34
|
+
# @since 0.1.0
|
35
|
+
# @api private
|
36
|
+
def initialize(options = {}, &blk)
|
37
|
+
super(options, &nil)
|
38
|
+
|
39
|
+
@default_scheme = options[:scheme] if options[:scheme]
|
40
|
+
@default_host = options[:host] if options[:host]
|
41
|
+
@default_port = options[:port] if options[:port]
|
42
|
+
@route_class = options[:route] || Routing::Route
|
43
|
+
@resolver = options[:resolver] || Routing::EndpointResolver.new(options)
|
44
|
+
end
|
45
|
+
|
46
|
+
# Separator between controller and action name.
|
47
|
+
#
|
48
|
+
# @see Lotus::Routing::EndpointResolver::ACTION_SEPARATOR
|
49
|
+
#
|
50
|
+
# @since 0.1.0
|
51
|
+
# @api private
|
52
|
+
def action_separator
|
53
|
+
@resolver.action_separator
|
54
|
+
end
|
55
|
+
|
56
|
+
# Finds a path from the given options.
|
57
|
+
#
|
58
|
+
# @see Lotus::Routing::EndpointResolver#find
|
59
|
+
#
|
60
|
+
# @since 0.1.0
|
61
|
+
# @api private
|
62
|
+
def find(options)
|
63
|
+
@resolver.find(options)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Generate a relative URL for a specified named route.
|
67
|
+
#
|
68
|
+
# @see Lotus::Router#path
|
69
|
+
#
|
70
|
+
# @since 0.1.0
|
71
|
+
# @api private
|
72
|
+
def path(route, *args)
|
73
|
+
_rescue_url_recognition { super }
|
74
|
+
end
|
75
|
+
|
76
|
+
# Generate an absolute URL for a specified named route.
|
77
|
+
#
|
78
|
+
# @see Lotus::Router#path
|
79
|
+
#
|
80
|
+
# @since 0.1.0
|
81
|
+
# @api private
|
82
|
+
def url(route, *args)
|
83
|
+
_rescue_url_recognition { super }
|
84
|
+
end
|
85
|
+
|
86
|
+
# Support for OPTIONS HTTP verb
|
87
|
+
#
|
88
|
+
# @see Lotus::Router#options
|
89
|
+
#
|
90
|
+
# @since 0.1.0
|
91
|
+
# @api private
|
92
|
+
def options(path, options = {}, &blk)
|
93
|
+
add_with_request_method(path, :options, options, &blk)
|
94
|
+
end
|
95
|
+
|
96
|
+
# @api private
|
97
|
+
def reset!
|
98
|
+
uncompile
|
99
|
+
@routes, @named_routes, @root = [], Hash.new{|h,k| h[k] = []}, Node::Root.new(self)
|
100
|
+
@default_host, @default_port, @default_scheme = 'localhost', 80, 'http'
|
101
|
+
end
|
102
|
+
|
103
|
+
# @api private
|
104
|
+
def pass_on_response(response)
|
105
|
+
super response.to_a
|
106
|
+
end
|
107
|
+
|
108
|
+
# @api private
|
109
|
+
def no_response(request, env)
|
110
|
+
if request.acceptable_methods.any? && !request.acceptable_methods.include?(env['REQUEST_METHOD'])
|
111
|
+
[405, {'Allow' => request.acceptable_methods.sort.join(", ")}, []]
|
112
|
+
else
|
113
|
+
@default_app.call(env)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
private
|
118
|
+
def add_with_request_method(path, method, opts = {}, &app)
|
119
|
+
super.generate(@resolver, opts, &app)
|
120
|
+
end
|
121
|
+
|
122
|
+
def _rescue_url_recognition
|
123
|
+
yield
|
124
|
+
rescue ::HttpRouter::InvalidRouteException,
|
125
|
+
::HttpRouter::TooManyParametersException => e
|
126
|
+
raise Routing::InvalidRouteException.new(e.message)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|