lotus-router 0.1.1 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|