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.
@@ -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