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.
@@ -1,5 +1,5 @@
1
1
  module Lotus
2
2
  class Router
3
- VERSION = '0.1.1'
3
+ VERSION = '0.2.0'.freeze
4
4
  end
5
5
  end
@@ -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: DashboardController::Index
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
- Utils::Class.load!(@name, @namespace).new
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
- # 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 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. Please note that this option override
70
- # :suffix. This string SHOULD contain `'%{controller}'` and `'%{action}'`,
71
- # all the other keys will be ignored. See the examples below.
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
- # # => Will look for:
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 simple pattern
77
+ # @example Specify custom pattern
123
78
  # require 'lotus/router'
124
79
  #
125
- # resolver = Lotus::Routing::EndpointResolver.new(pattern: 'Controllers::%{controller}::%{action}')
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
- # # => Will look for:
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
- # # => Will look for:
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] :prefix an optional path prefix
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: ArticlesController::Show
148
+ # router.get '/', to: Articles::Show
198
149
  #
199
- # @example Resolve with a path prefix
150
+ # @example Resolve a redirect with a namespace
200
151
  # require 'lotus/router'
201
152
  #
202
153
  # router = Lotus::Router.new
203
- # router.get '/dashboard', to: BackendApp.new, prefix: 'backend'
204
- # # => Will be available under '/backend/dashboard'
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] :prefix an optional path prefix
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
- if prefix = options[:prefix]
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 < ::Exception
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(prefix: @name)
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(prefix: @name)
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(prefix: @name), &endpoint)
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,17 @@
1
+ require 'json'
2
+
3
+ module Lotus
4
+ module Routing
5
+ module Parsing
6
+ class JsonParser < Parser
7
+ def mime_types
8
+ ['application/json']
9
+ end
10
+
11
+ def parse(body)
12
+ JSON.parse(body)
13
+ end
14
+ end
15
+ end
16
+ end
17
+ 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.merge(prefix: @name), &blk)
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.merge(prefix: @name), &blk)
69
+ self.class.collection.new(@router, @options, &blk)
70
70
  end
71
71
  end
72
72
  end