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.
@@ -0,0 +1,510 @@
1
+ require 'lotus/utils/class'
2
+ require 'lotus/utils/kernel'
3
+ require 'lotus/utils/string'
4
+
5
+ module Lotus
6
+ module Controller
7
+ # Configuration for the framework, controllers and actions.
8
+ #
9
+ # Lotus::Controller has its own global configuration that can be manipulated
10
+ # via `Lotus::Controller.configure`.
11
+ #
12
+ # Every time that `Lotus::Controller` and `Lotus::Action` are included, that
13
+ # global configuration is being copied to the recipient. The copy will
14
+ # inherit all the settings from the original, but all the subsequent changes
15
+ # aren't reflected from the parent to the children, and viceversa.
16
+ #
17
+ # This architecture allows to have a global configuration that capture the
18
+ # most common cases for an application, and let controllers and single
19
+ # actions to specify exceptions.
20
+ #
21
+ # @since 0.2.0
22
+ class Configuration
23
+ # Default HTTP code for server side errors
24
+ #
25
+ # @since 0.2.0
26
+ # @api private
27
+ DEFAULT_ERROR_CODE = 500
28
+
29
+ # Default Mime type to format mapping
30
+ #
31
+ # @since 0.2.0
32
+ # @api private
33
+ DEFAULT_FORMATS = {
34
+ 'application/octet-stream' => :all,
35
+ '*/*' => :all,
36
+ 'text/html' => :html
37
+ }.freeze
38
+
39
+ # Return a copy of the configuration of the framework instance associated
40
+ # with the given class.
41
+ #
42
+ # When multiple instances of Lotus::Controller are used in the same
43
+ # application, we want to make sure that a controller or an action will
44
+ # receive the expected configuration.
45
+ #
46
+ # @param base [Class, Module] a controller or an action
47
+ #
48
+ # @return [Lotus::Controller::Configuration] the configuration associated
49
+ # to the given class.
50
+ #
51
+ # @since 0.2.0
52
+ # @api private
53
+ #
54
+ # @example Direct usage of the framework
55
+ # require 'lotus/controller'
56
+ #
57
+ # class Show
58
+ # include Lotus::Action
59
+ # end
60
+ #
61
+ # Lotus::Controller::Configuration.for(Show)
62
+ # # => will duplicate from Lotus::Controller
63
+ #
64
+ # @example Multiple instances of the framework
65
+ # require 'lotus/controller'
66
+ #
67
+ # module MyApp
68
+ # Controller = Lotus::Controller.duplicate(self)
69
+ #
70
+ # module Controllers::Dashboard
71
+ # include MyApp::Controller
72
+ #
73
+ # action 'Index' do
74
+ # def call(params)
75
+ # # ...
76
+ # end
77
+ # end
78
+ # end
79
+ # end
80
+ #
81
+ # class Show
82
+ # include Lotus::Action
83
+ # end
84
+ #
85
+ # Lotus::Controller::Configuration.for(Show)
86
+ # # => will duplicate from Lotus::Controller
87
+ #
88
+ # Lotus::Controller::Configuration.for(MyApp::Controllers::Dashboard)
89
+ # # => will duplicate from MyApp::Controller
90
+ def self.for(base)
91
+ namespace = Utils::String.new(base).namespace
92
+ framework = Utils::Class.load!("(#{namespace}|Lotus)::Controller")
93
+ framework.configuration.duplicate
94
+ end
95
+
96
+ # Initialize a configuration instance
97
+ #
98
+ # @return [Lotus::Controller::Configuration] a new configuration's
99
+ # instance
100
+ #
101
+ # @since 0.2.0
102
+ def initialize
103
+ reset!
104
+ end
105
+
106
+ # @attr_writer handle_exceptions [TrueClass,FalseClass] Decide if handle
107
+ # exceptions with an HTTP status or not
108
+ #
109
+ # @since 0.2.0
110
+ #
111
+ # @see Lotus::Controller::Configuration#handle_exceptions
112
+ attr_writer :handle_exceptions
113
+
114
+ # Decide if handle exceptions with an HTTP status or let them uncaught
115
+ #
116
+ # If this value is set to `true`, the configured exceptions will return
117
+ # the specified HTTP status, the rest of them with `500`.
118
+ #
119
+ # If this value is set to `false`, the exceptions won't be caught.
120
+ #
121
+ # This is part of a DSL, for this reason when this method is called with
122
+ # an argument, it will set the corresponding instance variable. When
123
+ # called without, it will return the already set value, or the default.
124
+ #
125
+ # @overload handle_exceptions(value)
126
+ # Sets the given value
127
+ # @param value [TrueClass, FalseClass] true or false, default to true
128
+ #
129
+ # @overload handle_exceptions
130
+ # Gets the value
131
+ # @return [TrueClass, FalseClass]
132
+ #
133
+ # @since 0.2.0
134
+ #
135
+ # @see Lotus::Controller::Configuration#handle_exception
136
+ # @see Lotus::Controller#configure
137
+ # @see Lotus::Action::Throwable
138
+ # @see http://httpstatus.es/500
139
+ #
140
+ # @example Getting the value
141
+ # require 'lotus/controller'
142
+ #
143
+ # Lotus::Controller.configuration.handle_exceptions # => true
144
+ #
145
+ # @example Setting the value
146
+ # require 'lotus/controller'
147
+ #
148
+ # Lotus::Controller.configure do
149
+ # handle_exceptions false
150
+ # end
151
+ def handle_exceptions(value = nil)
152
+ if value.nil?
153
+ @handle_exceptions
154
+ else
155
+ @handle_exceptions = value
156
+ end
157
+ end
158
+
159
+ # Specify how to handle an exception with an HTTP status
160
+ #
161
+ # Raised exceptions will return the configured HTTP status, only if
162
+ # `handled_exceptions` is set on `true`.
163
+ #
164
+ # @param exception [Hash] the exception class must be the key and the HTTP
165
+ # status the value
166
+ #
167
+ # @since 0.2.0
168
+ #
169
+ # @see Lotus::Controller::Configuration#handle_exceptions
170
+ # @see Lotus::Controller#configure
171
+ # @see Lotus::Action::Throwable
172
+ #
173
+ # @example
174
+ # require 'lotus/controller'
175
+ #
176
+ # Lotus::Controller.configure do
177
+ # handle_exception ArgumentError => 400
178
+ # end
179
+ def handle_exception(exception)
180
+ @handled_exceptions.merge!(exception)
181
+ end
182
+
183
+ # Return the HTTP status for the given exception
184
+ #
185
+ # Raised exceptions will return the configured HTTP status, only if
186
+ # `handled_exceptions` is set on `true`.
187
+ #
188
+ # @param exception [Hash] the exception class must be the key and the HTTP
189
+ # status the value of the hash
190
+ #
191
+ # @since 0.2.0
192
+ # @api private
193
+ #
194
+ # @see Lotus::Controller::Configuration#handle_exception
195
+ def exception_code(exception)
196
+ @handled_exceptions.fetch(exception) { DEFAULT_ERROR_CODE }
197
+ end
198
+
199
+ # Specify which is the default action module to be included when we use
200
+ # the `Lotus::Controller.action` method.
201
+ #
202
+ # This setting is useful when we use multiple instances of the framework
203
+ # in the same process, so we want to ensure that the actions will include
204
+ # `MyApp::Action`, rather than `AnotherApp::Action`.
205
+ #
206
+ # If not set, the default value is `Lotus::Action`
207
+ #
208
+ # This is part of a DSL, for this reason when this method is called with
209
+ # an argument, it will set the corresponding instance variable. When
210
+ # called without, it will return the already set value, or the default.
211
+ #
212
+ # @overload action_module(value)
213
+ # Sets the given value
214
+ # @param value [Module] the module to be included in all the actions
215
+ #
216
+ # @overload action_module
217
+ # Gets the value
218
+ # @return [Module]
219
+ #
220
+ # @since 0.2.0
221
+ #
222
+ # @see Lotus::Controller::Dsl#action
223
+ # @see Lotus::Controller#duplicate
224
+ #
225
+ # @example Getting the value
226
+ # require 'lotus/controller'
227
+ #
228
+ # Lotus::Controller.configuration.action_module # => Lotus::Action
229
+ #
230
+ # @example Setting the value
231
+ # require 'lotus/controller'
232
+ #
233
+ # module MyAction
234
+ # end
235
+ #
236
+ # Lotus::Controller.configure do
237
+ # action_module MyAction
238
+ # end
239
+ #
240
+ # class DashboardController
241
+ # include Lotus::Controller
242
+ #
243
+ # # It includes MyAction, instead of Lotus::Action
244
+ # action 'Index' do
245
+ # def call(params)
246
+ # # ...
247
+ # end
248
+ # end
249
+ # end
250
+ #
251
+ # @example Duplicated framework
252
+ # require 'lotus/controller'
253
+ #
254
+ # module MyApp
255
+ # Controller = Lotus::Controller.duplicate(self)
256
+ #
257
+ # module Controllers::Dashboard
258
+ # include MyApp::Controller
259
+ #
260
+ # # It includes MyApp::Action, instead of Lotus::Action
261
+ # action 'Index' do
262
+ # def call(params)
263
+ # # ...
264
+ # end
265
+ # end
266
+ # end
267
+ # end
268
+ def action_module(value = nil)
269
+ if value.nil?
270
+ @action_module
271
+ else
272
+ @action_module = value
273
+ end
274
+ end
275
+
276
+ # Specify the default modules to be included when `Lotus::Action` (or the
277
+ # `action_module`) is included. This also works with
278
+ # `Lotus::Controller.action`.
279
+ #
280
+ # If not set, this option will be ignored.
281
+ #
282
+ # This is part of a DSL, for this reason when this method is called with
283
+ # an argument, it will set the corresponding instance variable. When
284
+ # called without, it will return the already set value, or the default.
285
+ #
286
+ # @overload modules(blk)
287
+ # Adds the given block
288
+ # @param value [Proc] specify the modules to be included
289
+ #
290
+ # @overload modules
291
+ # Gets the value
292
+ # @return [Array] the list of the specified procs
293
+ #
294
+ # @since 0.2.0
295
+ #
296
+ # @see Lotus::Controller::Dsl#action
297
+ # @see Lotus::Controller#duplicate
298
+ #
299
+ # @example Getting the value
300
+ # require 'lotus/controller'
301
+ #
302
+ # Lotus::Controller.configuration.modules # => []
303
+ #
304
+ # @example Setting the value
305
+ # require 'lotus/controller'
306
+ # require 'lotus/action/cookies'
307
+ # require 'lotus/action/session'
308
+ #
309
+ # Lotus::Controller.configure do
310
+ # modules do
311
+ # include Lotus::Action::Cookies
312
+ # include Lotus::Action::Session
313
+ # end
314
+ # end
315
+ #
316
+ # class DashboardController
317
+ # include Lotus::Controller
318
+ #
319
+ # # It includes:
320
+ # # * Lotus::Action
321
+ # # * Lotus::Action::Cookies
322
+ # # * Lotus::Action::Session
323
+ # action 'Index' do
324
+ # def call(params)
325
+ # # ...
326
+ # end
327
+ # end
328
+ # end
329
+ def modules(&blk)
330
+ if block_given?
331
+ @modules.push(blk)
332
+ else
333
+ @modules
334
+ end
335
+ end
336
+
337
+ # Register a format
338
+ #
339
+ # @param hash [Hash] the symbol format must be the key and the mime type
340
+ # string must be the value of the hash
341
+ #
342
+ # @since 0.2.0
343
+ #
344
+ # @see Lotus::Action::Mime
345
+ #
346
+ # @example
347
+ # require 'lotus/controller'
348
+ #
349
+ # Lotus::Controller.configure do
350
+ # format custom: 'application/custom'
351
+ # end
352
+ #
353
+ # class ArticlesController
354
+ # include Lotus::Controller
355
+ #
356
+ # action 'Index' do
357
+ # def call(params)
358
+ # # ...
359
+ # end
360
+ # end
361
+ #
362
+ # action 'Show' do
363
+ # def call(params)
364
+ # # ...
365
+ # self.format = :custom
366
+ # end
367
+ # end
368
+ # end
369
+ #
370
+ # action = ArticlesController::Index.new
371
+ #
372
+ # action.call({ 'HTTP_ACCEPT' => 'text/html' })
373
+ # # => Content-Type "text/html"
374
+ # action.format # => :html
375
+ #
376
+ # action.call({ 'HTTP_ACCEPT' => 'application/custom' })
377
+ # # => Content-Type "application/custom"
378
+ # action.format # => :custom
379
+ #
380
+ #
381
+ #
382
+ # action = ArticlesController::Show.new
383
+ #
384
+ # action.call({ 'HTTP_ACCEPT' => 'text/html' })
385
+ # # => Content-Type "application/custom"
386
+ # action.format # => :custom
387
+ def format(hash)
388
+ symbol, mime_type = *Utils::Kernel.Array(hash)
389
+
390
+ @formats.merge! Utils::Kernel.String(mime_type) =>
391
+ Utils::Kernel.Symbol(symbol)
392
+ end
393
+
394
+ # Set a format as default fallback for all the requests without a strict
395
+ # requirement for the mime type.
396
+ #
397
+ # The given format must be coercible to a symbol, and be a valid mime type
398
+ # alias. If it isn't, at the runtime the framework will raise a
399
+ # `Lotus::Controller::UnknownFormatError`.
400
+ #
401
+ # By default this value is nil.
402
+ #
403
+ # This is part of a DSL, for this reason when this method is called with
404
+ # an argument, it will set the corresponding instance variable. When
405
+ # called without, it will return the already set value, or the default.
406
+ #
407
+ # @overload default_format(format)
408
+ # Sets the given value
409
+ # @param format [#to_sym] the symbol format
410
+ # @raise [TypeError] if it cannot be coerced to a symbol
411
+ #
412
+ # @overload default_format
413
+ # Gets the value
414
+ # @return [Symbol,nil]
415
+ #
416
+ # @since 0.2.0
417
+ #
418
+ # @see Lotus::Action::Mime
419
+ #
420
+ # @example Getting the value
421
+ # require 'lotus/controller'
422
+ #
423
+ # Lotus::Controller.configuration.default_format # => nil
424
+ #
425
+ # @example Setting the value
426
+ # require 'lotus/controller'
427
+ #
428
+ # Lotus::Controller.configure do
429
+ # default_format :html
430
+ # end
431
+ def default_format(format = nil)
432
+ if format
433
+ @default_format = Utils::Kernel.Symbol(format)
434
+ else
435
+ @default_format
436
+ end
437
+ end
438
+
439
+ # Returns a format for the given mime type
440
+ #
441
+ # @param mime_type [#to_s,#to_str] A mime type
442
+ #
443
+ # @return [Symbol,nil] the corresponding format, if present
444
+ #
445
+ # @see Lotus::Controller::Configuration#format
446
+ #
447
+ # @since 0.2.0
448
+ # @api private
449
+ def format_for(mime_type)
450
+ @formats[mime_type]
451
+ end
452
+
453
+ # Returns a mime type for the given format
454
+ #
455
+ # @param format [#to_sym] a format
456
+ #
457
+ # @return [String,nil] the corresponding mime type, if present
458
+ def mime_type_for(format)
459
+ @formats.key(format)
460
+ end
461
+
462
+ # Duplicate by copying the settings in a new instance.
463
+ #
464
+ # @return [Lotus::Controller::Configuration] a copy of the configuration
465
+ #
466
+ # @since 0.2.0
467
+ # @api private
468
+ def duplicate
469
+ Configuration.new.tap do |c|
470
+ c.handle_exceptions = handle_exceptions
471
+ c.handled_exceptions = handled_exceptions.dup
472
+ c.action_module = action_module
473
+ c.modules = modules.dup
474
+ c.formats = formats.dup
475
+ c.default_format = default_format
476
+ end
477
+ end
478
+
479
+ # Reset all the values to the defaults
480
+ #
481
+ # @since 0.2.0
482
+ # @api private
483
+ def reset!
484
+ @handle_exceptions = true
485
+ @handled_exceptions = {}
486
+ @modules = []
487
+ @formats = DEFAULT_FORMATS.dup
488
+ @default_format = nil
489
+ @action_module = ::Lotus::Action
490
+ end
491
+
492
+ # Load the configuration for the given action
493
+ #
494
+ # @since 0.2.0
495
+ # @api private
496
+ def load!(base)
497
+ modules.each do |mod|
498
+ base.class_eval(&mod)
499
+ end
500
+ end
501
+
502
+ protected
503
+ attr_accessor :handled_exceptions
504
+ attr_accessor :formats
505
+ attr_writer :action_module
506
+ attr_writer :modules
507
+ attr_writer :default_format
508
+ end
509
+ end
510
+ end