lotus-controller 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -12,7 +12,7 @@ module Lotus
12
12
  # * Default: it returns the given hash as it is. It's useful for testing purposes.
13
13
  #
14
14
  # @since 0.1.0
15
- class Params < Utils::Hash
15
+ class Params
16
16
  # The key that returns raw input from the Rack env
17
17
  #
18
18
  # @since 0.1.0
@@ -24,6 +24,12 @@ module Lotus
24
24
  # @since 0.1.0
25
25
  ROUTER_PARAMS = 'router.params'.freeze
26
26
 
27
+ # @attr_reader env [Hash] the Rack env
28
+ #
29
+ # @since 0.2.0
30
+ # @api private
31
+ attr_reader :env
32
+
27
33
  # Initialize the params and freeze them.
28
34
  #
29
35
  # @param env [Hash] a Rack env or an hash of params.
@@ -32,13 +38,24 @@ module Lotus
32
38
  #
33
39
  # @since 0.1.0
34
40
  def initialize(env)
35
- super _extract(env)
36
- symbolize!
41
+ @env = env
42
+ @params = Utils::Hash.new(_extract).symbolize!
37
43
  freeze
38
44
  end
39
45
 
46
+ # Returns the object associated with the given key
47
+ #
48
+ # @param key [Symbol] the key
49
+ #
50
+ # @return [Object,nil] return the associated object, if found
51
+ #
52
+ # @since 0.2.0
53
+ def [](key)
54
+ @params[key]
55
+ end
56
+
40
57
  private
41
- def _extract(env)
58
+ def _extract
42
59
  {}.tap do |result|
43
60
  if env.has_key?(RACK_INPUT)
44
61
  result.merge! ::Rack::Request.new(env).params
@@ -4,10 +4,91 @@ module Lotus
4
4
  #
5
5
  # @since 0.1.0
6
6
  module Rack
7
+ # The default session key for Rack
8
+ #
9
+ # @since 0.1.0
10
+ # @api private
7
11
  SESSION_KEY = 'rack.session'.freeze
12
+
13
+ # The default HTTP response code
14
+ #
15
+ # @since 0.1.0
16
+ # @api private
8
17
  DEFAULT_RESPONSE_CODE = 200
18
+
19
+ # The default Rack response body
20
+ #
21
+ # @since 0.1.0
22
+ # @api private
9
23
  DEFAULT_RESPONSE_BODY = []
10
24
 
25
+ # Override Ruby's hook for modules.
26
+ # It includes basic Lotus::Action modules to the given class.
27
+ #
28
+ # @param base [Class] the target action
29
+ #
30
+ # @since 0.1.0
31
+ # @api private
32
+ #
33
+ # @see http://www.ruby-doc.org/core-2.1.2/Module.html#method-i-included
34
+ def self.included(base)
35
+ base.extend ClassMethods
36
+ end
37
+
38
+ module ClassMethods
39
+ # Use a Rack middleware as a before callback.
40
+ #
41
+ # The middleware will be used as it is, no matter if it's a class or an
42
+ # instance. If it needs to be initialized, please do it before to pass
43
+ # it as the argument of this method.
44
+ #
45
+ # At the runtime, the middleware be invoked with the raw Rack env.
46
+ #
47
+ # Multiple middlewares can be employed, just by using multiple times
48
+ # this method.
49
+ #
50
+ # @param middleware [#call] A Rack middleware
51
+ #
52
+ # @since 0.2.0
53
+ #
54
+ # @see Lotus::Action::Callbacks::ClassMethods#before
55
+ #
56
+ # @example Class Middleware
57
+ # require 'lotus/controller'
58
+ #
59
+ # class SessionsController
60
+ # include Lotus::Controller
61
+ #
62
+ # action 'Create' do
63
+ # use OmniAuth
64
+ #
65
+ # def call(params)
66
+ # # ...
67
+ # end
68
+ # end
69
+ # end
70
+ #
71
+ # @example Instance Middleware
72
+ # require 'lotus/controller'
73
+ #
74
+ # class SessionsController
75
+ # include Lotus::Controller
76
+ #
77
+ # action 'Create' do
78
+ # use XMiddleware.new('x', 123)
79
+ #
80
+ # def call(params)
81
+ # # ...
82
+ # end
83
+ # end
84
+ # end
85
+ def use(middleware)
86
+ before do |params|
87
+ middleware.call(params.env)
88
+ end
89
+ end
90
+ end
91
+
11
92
  protected
12
93
  # Sets the HTTP status code for the response
13
94
  #
@@ -4,6 +4,11 @@ module Lotus
4
4
  #
5
5
  # @since 0.1.0
6
6
  module Redirect
7
+ # The HTTP header for redirects
8
+ #
9
+ # @since 0.2.0
10
+ # @api private
11
+ LOCATION = 'Location'.freeze
7
12
 
8
13
  protected
9
14
 
@@ -26,7 +31,7 @@ module Lotus
26
31
  # end
27
32
  # end
28
33
  def redirect_to(url, status: 302)
29
- headers.merge!('Location' => url)
34
+ headers.merge!(LOCATION => url)
30
35
  self.status = status
31
36
  end
32
37
  end
@@ -9,6 +9,7 @@ module Lotus
9
9
  # The key that returns raw session from the Rack env
10
10
  #
11
11
  # @since 0.1.0
12
+ # @api private
12
13
  SESSION_KEY = 'rack.session'.freeze
13
14
 
14
15
  protected
@@ -8,38 +8,18 @@ module Lotus
8
8
  # @since 0.1.0
9
9
  #
10
10
  # @see Lotus::Action::Throwable::ClassMethods#handle_exception
11
- # @see Lotus::Action::Throwable#throw
11
+ # @see Lotus::Action::Throwable#halt
12
12
  # @see Lotus::Action::Throwable#status
13
13
  module Throwable
14
+ # @since 0.2.0
15
+ # @api private
16
+ RACK_ERRORS = 'rack.errors'.freeze
17
+
14
18
  def self.included(base)
15
- base.class_eval do
16
- extend ClassMethods
17
- end
19
+ base.extend ClassMethods
18
20
  end
19
21
 
20
22
  module ClassMethods
21
- def self.extended(base)
22
- base.class_eval do
23
- include Utils::ClassAttribute
24
-
25
- # Action handled exceptions.
26
- #
27
- # When an handled exception is raised during #call execution, it will be
28
- # translated into the associated HTTP status.
29
- #
30
- # By default there aren't handled exceptions, all the errors are threaded
31
- # as a Server Side Error (500).
32
- #
33
- # @api private
34
- # @since 0.1.0
35
- #
36
- # @see Lotus::Controller.handled_exceptions
37
- # @see Lotus::Action::Throwable.handle_exception
38
- class_attribute :handled_exceptions
39
- self.handled_exceptions = Controller.handled_exceptions.dup
40
- end
41
- end
42
-
43
23
  protected
44
24
 
45
25
  # Handle the given exception with an HTTP status code.
@@ -50,8 +30,8 @@ module Lotus
50
30
  # This is a fine grained control, for a global configuration see
51
31
  # Lotus::Action.handled_exceptions
52
32
  #
53
- # @param exception [Class] the exception class
54
- # @param status [Fixmun] a valid HTTP status
33
+ # @param exception [Hash] the exception class must be the key and the
34
+ # HTTP status the value of the hash
55
35
  #
56
36
  # @since 0.1.0
57
37
  #
@@ -62,7 +42,7 @@ module Lotus
62
42
  #
63
43
  # class Show
64
44
  # include Lotus::Action
65
- # handle_exception RecordNotFound, 404
45
+ # handle_exception RecordNotFound => 404
66
46
  #
67
47
  # def call(params)
68
48
  # # ...
@@ -71,14 +51,14 @@ module Lotus
71
51
  # end
72
52
  #
73
53
  # Show.new.call({id: 1}) # => [404, {}, ['Not Found']]
74
- def handle_exception(exception, status)
75
- self.handled_exceptions[exception] = status
54
+ def handle_exception(exception)
55
+ configuration.handle_exception(exception)
76
56
  end
77
57
  end
78
58
 
79
59
  protected
80
60
 
81
- # Throw the given HTTP status code.
61
+ # Halt the action execution with the given HTTP status code.
82
62
  #
83
63
  # When used, the execution of a callback or of an action is interrupted
84
64
  # and the control returns to the framework, that decides how to handle
@@ -89,14 +69,14 @@ module Lotus
89
69
  #
90
70
  # @param code [Fixnum] a valid HTTP status code
91
71
  #
92
- # @since 0.1.0
72
+ # @since 0.2.0
93
73
  #
94
74
  # @see Lotus::Controller#handled_exceptions
95
75
  # @see Lotus::Action::Throwable#handle_exception
96
76
  # @see Lotus::Http::Status:ALL
97
- def throw(code)
77
+ def halt(code)
98
78
  status(*Http::Status.for_code(code))
99
- super :halt
79
+ throw :halt
100
80
  end
101
81
 
102
82
  # Sets the given code and message for the response
@@ -112,18 +92,39 @@ module Lotus
112
92
  end
113
93
 
114
94
  private
95
+ # @since 0.1.0
96
+ # @api private
115
97
  def _rescue
116
98
  catch :halt do
117
99
  begin
118
100
  yield
119
101
  rescue => exception
102
+ _reference_in_rack_errors(exception)
120
103
  _handle_exception(exception)
121
104
  end
122
105
  end
123
106
  end
124
107
 
108
+ # @since 0.2.0
109
+ # @api private
110
+ def _reference_in_rack_errors(exception)
111
+ if errors = @_env[RACK_ERRORS]
112
+ errors.write(_dump_exception(exception))
113
+ errors.flush
114
+ end
115
+ end
116
+
117
+ # @since 0.2.0
118
+ # @api private
119
+ def _dump_exception(exception)
120
+ [[exception.class, exception.message].compact.join(": "), *exception.backtrace].join("\n\t")
121
+ end
122
+
123
+ # @since 0.1.0
124
+ # @api private
125
125
  def _handle_exception(exception)
126
- throw self.class.handled_exceptions.fetch(exception.class, 500)
126
+ raise unless configuration.handle_exceptions
127
+ halt configuration.exception_code(exception.class)
127
128
  end
128
129
  end
129
130
  end
@@ -1,5 +1,6 @@
1
1
  require 'lotus/utils/class_attribute'
2
2
  require 'lotus/action'
3
+ require 'lotus/controller/configuration'
3
4
  require 'lotus/controller/dsl'
4
5
  require 'lotus/controller/version'
5
6
  require 'rack-patch'
@@ -26,43 +27,241 @@ module Lotus
26
27
  # end
27
28
  # end
28
29
  module Controller
30
+ # Unknown format error
31
+ #
32
+ # This error is raised when a action sets a format that it isn't recognized
33
+ # both by `Lotus::Controller::Configuration` and the list of Rack mime types
34
+ #
35
+ # @since 0.2.0
36
+ #
37
+ # @see Lotus::Action::Mime#format=
38
+ class UnknownFormatError < ::StandardError
39
+ def initialize(format)
40
+ super("Cannot find a corresponding Mime type for '#{ format }'. Please configure it with Lotus::Controller::Configuration#format.")
41
+ end
42
+ end
43
+
29
44
  include Utils::ClassAttribute
30
45
 
31
- # Global handled exceptions.
32
- # When an handled exception is raised during #call execution, it will be
33
- # translated into the associated HTTP status.
46
+ # Framework configuration
34
47
  #
35
- # By default there aren't handled exceptions, all the errors are threaded
36
- # as a Server Side Error (500).
48
+ # @since 0.2.0
49
+ # @api private
50
+ class_attribute :configuration
51
+ self.configuration = Configuration.new
52
+
53
+ # Configure the framework.
54
+ # It yields the given block in the context of the configuration
37
55
  #
38
- # **Important:** Be sure to set this configuration, **before** the actions
39
- # and controllers of your application are loaded.
56
+ # @param blk [Proc] the configuration block
40
57
  #
41
- # @since 0.1.0
58
+ # @since 0.2.0
42
59
  #
43
- # @see Lotus::Action::Throwable
60
+ # @see Lotus::Controller::Configuration
44
61
  #
45
62
  # @example
46
63
  # require 'lotus/controller'
47
64
  #
48
- # Lotus::Controller.handled_exceptions = { RecordNotFound => 404 }
65
+ # Lotus::Controller.configure do
66
+ # handle_exceptions false
67
+ # end
68
+ def self.configure(&blk)
69
+ configuration.instance_eval(&blk)
70
+ end
71
+
72
+ # Duplicate Lotus::Controller in order to create a new separated instance
73
+ # of the framework.
74
+ #
75
+ # The new instance of the framework will be completely decoupled from the
76
+ # original. It will inherit the configuration, but all the changes that
77
+ # happen after the duplication, won't be reflected on the other copies.
78
+ #
79
+ # @return [Module] a copy of Lotus::Controller
80
+ #
81
+ # @since 0.2.0
82
+ # @api private
83
+ #
84
+ # @example Basic usage
85
+ # require 'lotus/controller'
86
+ #
87
+ # module MyApp
88
+ # Controller = Lotus::Controller.dupe
89
+ # end
90
+ #
91
+ # MyApp::Controller == Lotus::Controller # => false
92
+ #
93
+ # MyApp::Controller.configuration ==
94
+ # Lotus::Controller.configuration # => false
95
+ #
96
+ # @example Inheriting configuration
97
+ # require 'lotus/controller'
98
+ #
99
+ # Lotus::Controller.configure do
100
+ # handle_exceptions false
101
+ # end
102
+ #
103
+ # module MyApp
104
+ # Controller = Lotus::Controller.dupe
105
+ # end
106
+ #
107
+ # module MyApi
108
+ # Controller = Lotus::Controller.dupe
109
+ # Controller.configure do
110
+ # handle_exceptions true
111
+ # end
112
+ # end
113
+ #
114
+ # Lotus::Controller.configuration.handle_exceptions # => false
115
+ # MyApp::Controller.configuration.handle_exceptions # => false
116
+ # MyApi::Controller.configuration.handle_exceptions # => true
117
+ def self.dupe
118
+ dup.tap do |duplicated|
119
+ duplicated.configuration = configuration.duplicate
120
+ end
121
+ end
122
+
123
+ # Duplicate the framework and generate modules for the target application
124
+ #
125
+ # @param mod [Module] the Ruby namespace of the application
126
+ # @param controllers [String] the optional namespace where the application's
127
+ # controllers will live
128
+ # @param blk [Proc] an optional block to configure the framework
129
+ #
130
+ # @return [Module] a copy of Lotus::Controller
131
+ #
132
+ # @since 0.2.0
133
+ #
134
+ # @see Lotus::Controller#dupe
135
+ # @see Lotus::Controller::Configuration
136
+ # @see Lotus::Controller::Configuration#action_module
137
+ #
138
+ # @example Basic usage
139
+ # require 'lotus/controller'
140
+ #
141
+ # module MyApp
142
+ # Controller = Lotus::Controller.duplicate(self)
143
+ # end
144
+ #
145
+ # # It will:
146
+ # #
147
+ # # 1. Generate MyApp::Controller
148
+ # # 2. Generate MyApp::Action
149
+ # # 3. Generate MyApp::Controllers
150
+ # # 4. Configure MyApp::Action as the default module for actions
151
+ #
152
+ # module MyApp::Controllers::Dashboard
153
+ # include MyApp::Controller
49
154
  #
50
- # class Show
51
- # include Lotus::Action
155
+ # action 'Index' do # this will inject MyApp::Action
156
+ # def call(params)
157
+ # # ...
158
+ # end
159
+ # end
160
+ # end
52
161
  #
53
- # def call(params)
162
+ # @example Compare code
163
+ # require 'lotus/controller'
164
+ #
165
+ # module MyApp
166
+ # Controller = Lotus::Controller.duplicate(self) do
54
167
  # # ...
55
- # raise RecordNotFound.new
56
168
  # end
57
169
  # end
58
170
  #
59
- # Show.new.call({id: 1}) # => [404, {}, ['Not Found']]
60
- class_attribute :handled_exceptions
61
- self.handled_exceptions = {}
171
+ # # it's equivalent to:
172
+ #
173
+ # module MyApp
174
+ # Controller = Lotus::Controller.dupe
175
+ # Action = Lotus::Action.dup
176
+ #
177
+ # module Controllers
178
+ # end
179
+ #
180
+ # Controller.configure do
181
+ # action_module MyApp::Action
182
+ # end
183
+ #
184
+ # Controller.configure do
185
+ # # ...
186
+ # end
187
+ # end
188
+ #
189
+ # @example Custom controllers module
190
+ # require 'lotus/controller'
191
+ #
192
+ # module MyApp
193
+ # Controller = Lotus::Controller.duplicate(self, 'Ctrls')
194
+ # end
195
+ #
196
+ # defined?(MyApp::Controllers) # => nil
197
+ # defined?(MyApp::Ctrls) # => "constant"
198
+ #
199
+ # # Developers can namespace controllers under Ctrls
200
+ # module MyApp::Ctrls::Dashboard
201
+ # # ...
202
+ # end
203
+ #
204
+ # @example Nil controllers module
205
+ # require 'lotus/controller'
206
+ #
207
+ # module MyApp
208
+ # Controller = Lotus::Controller.duplicate(self, nil)
209
+ # end
210
+ #
211
+ # defined?(MyApp::Controllers) # => nil
212
+ #
213
+ # # Developers can namespace controllers under MyApp
214
+ # module MyApp::DashboardController
215
+ # # ...
216
+ # end
217
+ #
218
+ # @example Block usage
219
+ # require 'lotus/controller'
220
+ #
221
+ # module MyApp
222
+ # Controller = Lotus::Controller.duplicate(self) do
223
+ # handle_exceptions false
224
+ # end
225
+ # end
226
+ #
227
+ # Lotus::Controller.configuration.handle_exceptions # => true
228
+ # MyApp::Controller.configuration.handle_exceptions # => false
229
+ def self.duplicate(mod, controllers = 'Controllers', &blk)
230
+ dupe.tap do |duplicated|
231
+ mod.module_eval %{ module #{ controllers }; end } if controllers
232
+ mod.module_eval %{ Action = Lotus::Action.dup }
233
+
234
+ duplicated.module_eval %{
235
+ configure do
236
+ action_module #{ mod }::Action
237
+ end
238
+ }
62
239
 
240
+ duplicated.configure(&blk) if block_given?
241
+ end
242
+ end
243
+
244
+ # Override Ruby's hook for modules.
245
+ # It includes basic Lotus::Controller modules to the given Class (or Module).
246
+ # It sets a copy of the framework configuration
247
+ #
248
+ # @param base [Class,Module] the target controller
249
+ #
250
+ # @since 0.1.0
251
+ # @api private
252
+ #
253
+ # @see http://www.ruby-doc.org/core-2.1.2/Module.html#method-i-included
254
+ #
255
+ # @see Lotus::Controller::Dsl
63
256
  def self.included(base)
257
+ conf = self.configuration.duplicate
258
+
64
259
  base.class_eval do
65
260
  include Dsl
261
+ include Utils::ClassAttribute
262
+
263
+ class_attribute :configuration
264
+ self.configuration = conf
66
265
  end
67
266
  end
68
267
  end