lotus-router 0.1.1 → 0.2.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/CHANGELOG.md +49 -42
- data/README.md +55 -30
- data/lib/lotus/router.rb +234 -126
- data/lib/lotus/router/version.rb +1 -1
- data/lib/lotus/routing/endpoint.rb +38 -2
- data/lib/lotus/routing/endpoint_resolver.rb +24 -73
- data/lib/lotus/routing/http_router.rb +8 -1
- data/lib/lotus/routing/namespace.rb +3 -3
- data/lib/lotus/routing/parsers.rb +71 -0
- data/lib/lotus/routing/parsing/json_parser.rb +17 -0
- data/lib/lotus/routing/parsing/parser.rb +43 -0
- data/lib/lotus/routing/resource.rb +4 -4
- data/lib/lotus/routing/resource/action.rb +34 -36
- data/lib/lotus/routing/resources/action.rb +13 -6
- data/lib/lotus/routing/route.rb +9 -0
- data/lib/lotus/routing/routes_inspector.rb +168 -0
- data/lotus-router.gemspec +5 -4
- metadata +19 -9
data/lib/lotus/router/version.rb
CHANGED
@@ -30,6 +30,20 @@ module Lotus
|
|
30
30
|
# get '/rack-app', to: RackApp.new
|
31
31
|
# end
|
32
32
|
class Endpoint < SimpleDelegator
|
33
|
+
# @since 0.2.0
|
34
|
+
def inspect
|
35
|
+
case __getobj__
|
36
|
+
when Proc
|
37
|
+
source, line = __getobj__.source_location
|
38
|
+
lambda_inspector = " (lambda)" if __getobj__.lambda?
|
39
|
+
|
40
|
+
"#<Proc@#{ ::File.expand_path(source) }:#{ line }#{ lambda_inspector }>"
|
41
|
+
when Class
|
42
|
+
__getobj__
|
43
|
+
else
|
44
|
+
"#<#{ __getobj__.class }>"
|
45
|
+
end
|
46
|
+
end
|
33
47
|
end
|
34
48
|
|
35
49
|
# Routing endpoint
|
@@ -52,7 +66,7 @@ module Lotus
|
|
52
66
|
#
|
53
67
|
# Lotus::Router.new do
|
54
68
|
# get '/class', to: RackMiddleware
|
55
|
-
# get '/lotus-action-class', to:
|
69
|
+
# get '/lotus-action-class', to: Dashboard::Index
|
56
70
|
# get '/lotus-action-string', to: 'dashboard#index'
|
57
71
|
#
|
58
72
|
# resource 'identity'
|
@@ -103,9 +117,31 @@ module Lotus
|
|
103
117
|
obj.call(env)
|
104
118
|
end
|
105
119
|
|
120
|
+
# @since 0.2.0
|
121
|
+
def inspect
|
122
|
+
# TODO review this implementation once the namespace feature will be
|
123
|
+
# cleaned up.
|
124
|
+
result = klass rescue nil
|
125
|
+
|
126
|
+
if result.nil?
|
127
|
+
result = @name
|
128
|
+
result = "#{ @namespace }::#{ result }" if @namespace != Object
|
129
|
+
end
|
130
|
+
|
131
|
+
result
|
132
|
+
end
|
133
|
+
|
106
134
|
private
|
135
|
+
# @since 0.1.0
|
136
|
+
# @api private
|
107
137
|
def obj
|
108
|
-
|
138
|
+
klass.new
|
139
|
+
end
|
140
|
+
|
141
|
+
# @since 0.2.0
|
142
|
+
# @api private
|
143
|
+
def klass
|
144
|
+
Utils::Class.load!(@name, @namespace)
|
109
145
|
rescue NameError => e
|
110
146
|
raise EndpointNotFound.new(e.message)
|
111
147
|
end
|
@@ -10,29 +10,9 @@ module Lotus
|
|
10
10
|
#
|
11
11
|
# @api private
|
12
12
|
class EndpointResolver
|
13
|
-
#
|
14
|
-
#
|
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 resolver 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
|
13
|
+
# @since 0.2.0
|
14
|
+
# @api private
|
15
|
+
NAMING_PATTERN = '%{controller}::%{action}'.freeze
|
36
16
|
|
37
17
|
# Default separator for controller and action.
|
38
18
|
# A different separator can be passed to #initialize with the `:separator` option.
|
@@ -62,27 +42,19 @@ module Lotus
|
|
62
42
|
# @option options [Class,Module] :namespace the Ruby namespace where to
|
63
43
|
# lookup for controllers and actions. (defaults to `Object`)
|
64
44
|
#
|
65
|
-
# @option options [String] :suffix the suffix appended to the controller
|
66
|
-
# name during the lookup. (defaults to `SUFFIX`)
|
67
|
-
#
|
68
45
|
# @option options [String] :pattern the string to interpolate in order
|
69
|
-
# to return an action name.
|
70
|
-
#
|
71
|
-
#
|
46
|
+
# to return an action name. This string SHOULD contain
|
47
|
+
# <tt>'%{controller}'</tt> and <tt>'%{action}'</tt>, all the other keys
|
48
|
+
# will be ignored.
|
49
|
+
# See the examples below.
|
72
50
|
#
|
73
51
|
# @option options [String] :action_separator the sepatator between controller and
|
74
52
|
# action name. (defaults to `ACTION_SEPARATOR`)
|
75
53
|
#
|
76
|
-
#
|
77
|
-
#
|
78
54
|
# @return [Lotus::Routing::EndpointResolver] self
|
79
55
|
#
|
80
|
-
#
|
81
|
-
#
|
82
56
|
# @since 0.1.0
|
83
57
|
#
|
84
|
-
#
|
85
|
-
#
|
86
58
|
# @example Specify custom endpoint class
|
87
59
|
# require 'lotus/router'
|
88
60
|
#
|
@@ -91,8 +63,6 @@ module Lotus
|
|
91
63
|
#
|
92
64
|
# router.get('/', to: endpoint).dest # => #<CustomEndpoint:0x007f97f3359570 ...>
|
93
65
|
#
|
94
|
-
#
|
95
|
-
#
|
96
66
|
# @example Specify custom Ruby namespace
|
97
67
|
# require 'lotus/router'
|
98
68
|
#
|
@@ -100,34 +70,18 @@ module Lotus
|
|
100
70
|
# router = Lotus::Router.new(resolver: resolver)
|
101
71
|
#
|
102
72
|
# router.get('/', to: 'articles#show')
|
103
|
-
#
|
104
|
-
# # * MyApp::Articles::Controller::Show
|
105
|
-
# # * MyApp::ArticlesController::Show
|
106
|
-
#
|
107
|
-
#
|
108
|
-
#
|
109
|
-
# @example Specify custom controller suffix
|
110
|
-
# require 'lotus/router'
|
111
|
-
#
|
112
|
-
# resolver = Lotus::Routing::EndpointResolver.new(suffix: '(Controller::|Ctrl::)')
|
113
|
-
# router = Lotus::Router.new(resolver: resolver)
|
114
|
-
#
|
115
|
-
# router.get('/', to: 'articles#show')
|
116
|
-
# # => Will look for:
|
117
|
-
# # * ArticlesController::Show
|
118
|
-
# # * ArticlesCtrl::Show
|
73
|
+
# # => Will look for: MyApp::Articles::Show
|
119
74
|
#
|
120
75
|
#
|
121
76
|
#
|
122
|
-
# @example Specify custom
|
77
|
+
# @example Specify custom pattern
|
123
78
|
# require 'lotus/router'
|
124
79
|
#
|
125
|
-
# resolver = Lotus::Routing::EndpointResolver.new(pattern: '
|
80
|
+
# resolver = Lotus::Routing::EndpointResolver.new(pattern: '%{controller}Controller::%{action}')
|
126
81
|
# router = Lotus::Router.new(resolver: resolver)
|
127
82
|
#
|
128
83
|
# router.get('/', to: 'articles#show')
|
129
|
-
#
|
130
|
-
# # * Controllers::Articles::Show
|
84
|
+
# # => Will look for: ArticlesController::Show
|
131
85
|
#
|
132
86
|
#
|
133
87
|
#
|
@@ -138,15 +92,12 @@ module Lotus
|
|
138
92
|
# router = Lotus::Router.new(resolver: resolver)
|
139
93
|
#
|
140
94
|
# router.get('/', to: 'articles@show')
|
141
|
-
#
|
142
|
-
# # * Articles::Controller::Show
|
143
|
-
# # * ArticlesController::Show
|
95
|
+
# # => Will look for: Articles::Show
|
144
96
|
def initialize(options = {})
|
145
97
|
@endpoint_class = options[:endpoint] || Endpoint
|
146
98
|
@namespace = options[:namespace] || Object
|
147
99
|
@action_separator = options[:action_separator] || ACTION_SEPARATOR
|
148
|
-
@pattern = options[:pattern] ||
|
149
|
-
"%{controller}#{options[:suffix] || SUFFIX}%{action}"
|
100
|
+
@pattern = options[:pattern] || NAMING_PATTERN
|
150
101
|
end
|
151
102
|
|
152
103
|
# Resolve the given set of HTTP verb, path, endpoint and options.
|
@@ -156,7 +107,7 @@ module Lotus
|
|
156
107
|
# @param options [Hash] the options required to resolve the endpoint
|
157
108
|
#
|
158
109
|
# @option options [String,Proc,Class,Object#call] :to the endpoint
|
159
|
-
# @option options [String] :
|
110
|
+
# @option options [String] :namespace an optional routing namespace
|
160
111
|
#
|
161
112
|
# @return [Endpoint] this may vary according to the :endpoint option
|
162
113
|
# passed to #initialize
|
@@ -194,14 +145,18 @@ module Lotus
|
|
194
145
|
# require 'lotus/router'
|
195
146
|
#
|
196
147
|
# router = Lotus::Router.new
|
197
|
-
# router.get '/', to:
|
148
|
+
# router.get '/', to: Articles::Show
|
198
149
|
#
|
199
|
-
# @example Resolve with a
|
150
|
+
# @example Resolve a redirect with a namespace
|
200
151
|
# require 'lotus/router'
|
201
152
|
#
|
202
153
|
# router = Lotus::Router.new
|
203
|
-
# router.
|
204
|
-
#
|
154
|
+
# router.namespace 'users' do
|
155
|
+
# get '/home', to: ->(env) { ... }
|
156
|
+
# redirect '/dashboard', to: '/home'
|
157
|
+
# end
|
158
|
+
#
|
159
|
+
# # GET /users/dashboard => 301 Location: "/users/home"
|
205
160
|
def resolve(options, &endpoint)
|
206
161
|
result = endpoint || find(options)
|
207
162
|
resolve_callable(result) || resolve_matchable(result) || default
|
@@ -211,17 +166,13 @@ module Lotus
|
|
211
166
|
#
|
212
167
|
# @param options [Hash] the path description
|
213
168
|
# @option options [String,Proc,Class,Object#call] :to the endpoint
|
214
|
-
# @option options [String] :
|
169
|
+
# @option options [String] :namespace an optional namespace
|
215
170
|
#
|
216
171
|
# @since 0.1.0
|
217
172
|
#
|
218
173
|
# @return [Object]
|
219
174
|
def find(options)
|
220
|
-
|
221
|
-
prefix.join(options[:to])
|
222
|
-
else
|
223
|
-
options[:to]
|
224
|
-
end
|
175
|
+
options[:to]
|
225
176
|
end
|
226
177
|
|
227
178
|
protected
|
@@ -2,6 +2,7 @@ require 'http_router'
|
|
2
2
|
require 'lotus/utils/io'
|
3
3
|
require 'lotus/routing/endpoint_resolver'
|
4
4
|
require 'lotus/routing/route'
|
5
|
+
require 'lotus/routing/parsers'
|
5
6
|
|
6
7
|
Lotus::Utils::IO.silence_warnings do
|
7
8
|
HttpRouter::Route::VALID_HTTP_VERBS = %w{GET POST PUT PATCH DELETE HEAD OPTIONS TRACE}
|
@@ -14,7 +15,7 @@ module Lotus
|
|
14
15
|
# given arguments.
|
15
16
|
#
|
16
17
|
# @since 0.1.0
|
17
|
-
class InvalidRouteException < ::
|
18
|
+
class InvalidRouteException < ::StandardError
|
18
19
|
end
|
19
20
|
|
20
21
|
# HTTP router
|
@@ -41,6 +42,7 @@ module Lotus
|
|
41
42
|
@default_port = options[:port] if options[:port]
|
42
43
|
@route_class = options[:route] || Routing::Route
|
43
44
|
@resolver = options[:resolver] || Routing::EndpointResolver.new(options)
|
45
|
+
@parsers = Routing::Parsers.new(options[:parsers])
|
44
46
|
end
|
45
47
|
|
46
48
|
# Separator between controller and action name.
|
@@ -105,6 +107,11 @@ module Lotus
|
|
105
107
|
)
|
106
108
|
end
|
107
109
|
|
110
|
+
# @api private
|
111
|
+
def raw_call(env, &blk)
|
112
|
+
super(@parsers.call(env))
|
113
|
+
end
|
114
|
+
|
108
115
|
# @api private
|
109
116
|
def reset!
|
110
117
|
uncompile
|
@@ -60,19 +60,19 @@ module Lotus
|
|
60
60
|
# @api private
|
61
61
|
# @since 0.1.0
|
62
62
|
def resource(name, options = {})
|
63
|
-
super name, options.merge(
|
63
|
+
super name, options.merge(namespace: @name.relative_join(options[:namespace]))
|
64
64
|
end
|
65
65
|
|
66
66
|
# @api private
|
67
67
|
# @since 0.1.0
|
68
68
|
def resources(name, options = {})
|
69
|
-
super name, options.merge(
|
69
|
+
super name, options.merge(namespace: @name.relative_join(options[:namespace]))
|
70
70
|
end
|
71
71
|
|
72
72
|
# @api private
|
73
73
|
# @since 0.1.0
|
74
74
|
def redirect(path, options = {}, &endpoint)
|
75
|
-
super(@name.join(path), options.merge(
|
75
|
+
super(@name.join(path), options.merge(to: @name.join(options[:to])), &endpoint)
|
76
76
|
end
|
77
77
|
|
78
78
|
# Supports nested namespaces
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'lotus/routing/parsing/parser'
|
2
|
+
|
3
|
+
module Lotus
|
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,43 @@
|
|
1
|
+
require 'lotus/utils/class'
|
2
|
+
require 'lotus/utils/string'
|
3
|
+
|
4
|
+
module Lotus
|
5
|
+
module Routing
|
6
|
+
module Parsing
|
7
|
+
class UnknownParserError < ::StandardError
|
8
|
+
def initialize(parser)
|
9
|
+
super("Unknown Parser: `#{ parser }'")
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class Parser
|
14
|
+
def self.for(parser)
|
15
|
+
case parser
|
16
|
+
when String, Symbol
|
17
|
+
require_parser(parser)
|
18
|
+
else
|
19
|
+
parser
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def mime_types
|
24
|
+
raise NotImplementedError
|
25
|
+
end
|
26
|
+
|
27
|
+
def parse(body)
|
28
|
+
Hash.new
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
def self.require_parser(parser)
|
33
|
+
require "lotus/routing/parsing/#{ parser }_parser"
|
34
|
+
|
35
|
+
parser = Utils::String.new(parser).classify
|
36
|
+
Utils::Class.load!("Lotus::Routing::Parsing::#{ parser }Parser").new
|
37
|
+
rescue LoadError, NameError
|
38
|
+
raise UnknownParserError.new(parser)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -54,19 +54,19 @@ module Lotus
|
|
54
54
|
|
55
55
|
private
|
56
56
|
def generate(&blk)
|
57
|
+
instance_eval(&blk) if block_given?
|
58
|
+
|
57
59
|
@options.actions.each do |action|
|
58
60
|
self.class.action.generate(@router, action, @options)
|
59
61
|
end
|
60
|
-
|
61
|
-
instance_eval(&blk) if block_given?
|
62
62
|
end
|
63
63
|
|
64
64
|
def member(&blk)
|
65
|
-
self.class.member.new(@router, @options
|
65
|
+
self.class.member.new(@router, @options, &blk)
|
66
66
|
end
|
67
67
|
|
68
68
|
def collection(&blk)
|
69
|
-
self.class.collection.new(@router, @options
|
69
|
+
self.class.collection.new(@router, @options, &blk)
|
70
70
|
end
|
71
71
|
end
|
72
72
|
end
|