lotus-controller 0.1.0 → 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.
@@ -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