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.
data/lib/lotus/action.rb CHANGED
@@ -1,3 +1,4 @@
1
+ require 'lotus/action/configurable'
1
2
  require 'lotus/action/rack'
2
3
  require 'lotus/action/mime'
3
4
  require 'lotus/action/redirect'
@@ -22,8 +23,27 @@ module Lotus
22
23
  # end
23
24
  # end
24
25
  module Action
26
+ # Override Ruby's hook for modules.
27
+ # It includes basic Lotus::Action modules to the given class.
28
+ #
29
+ # @param base [Class] the target action
30
+ #
31
+ # @since 0.1.0
32
+ # @api private
33
+ #
34
+ # @see http://www.ruby-doc.org/core-2.1.2/Module.html#method-i-included
35
+ #
36
+ # @see Lotus::Action::Configurable
37
+ # @see Lotus::Action::Rack
38
+ # @see Lotus::Action::Mime
39
+ # @see Lotus::Action::Redirect
40
+ # @see Lotus::Action::Exposable
41
+ # @see Lotus::Action::Throwable
42
+ # @see Lotus::Action::Callbacks
43
+ # @see Lotus::Action::Callable
25
44
  def self.included(base)
26
45
  base.class_eval do
46
+ include Configurable
27
47
  include Rack
28
48
  include Mime
29
49
  include Redirect
@@ -9,6 +9,15 @@ module Lotus
9
9
  # @see Lotus::Action::ClassMethods#before
10
10
  # @see Lotus::Action::ClassMethods#after
11
11
  module Callbacks
12
+ # Override Ruby's hook for modules.
13
+ # It includes callbacks logic
14
+ #
15
+ # @param base [Class] the target action
16
+ #
17
+ # @since 0.1.0
18
+ # @api private
19
+ #
20
+ # @see http://www.ruby-doc.org/core-2.1.2/Module.html#method-i-included
12
21
  def self.included(base)
13
22
  base.class_eval do
14
23
  extend ClassMethods
@@ -17,6 +26,15 @@ module Lotus
17
26
  end
18
27
 
19
28
  module ClassMethods
29
+ # Override Ruby's hook for modules.
30
+ # It includes callbacks logic
31
+ #
32
+ # @param base [Class] the target action
33
+ #
34
+ # @since 0.1.0
35
+ # @api private
36
+ #
37
+ # @see http://www.ruby-doc.org/core-2.1.2/Module.html#method-i-extended
20
38
  def self.extended(base)
21
39
  base.class_eval do
22
40
  include Utils::ClassAttribute
@@ -90,7 +108,7 @@ module Lotus
90
108
  # # 2. set the article
91
109
  # # 3. #call
92
110
  def before(*callbacks, &blk)
93
- before_callbacks.add *callbacks, &blk
111
+ before_callbacks.add(*callbacks, &blk)
94
112
  end
95
113
 
96
114
  # Define a callback for an Action.
@@ -109,7 +127,7 @@ module Lotus
109
127
  #
110
128
  # @see Lotus::Action::Callbacks::ClassMethods#before
111
129
  def after(*callbacks, &blk)
112
- after_callbacks.add *callbacks, &blk
130
+ after_callbacks.add(*callbacks, &blk)
113
131
  end
114
132
  end
115
133
 
@@ -0,0 +1,48 @@
1
+ require 'lotus/utils/class_attribute'
2
+
3
+ module Lotus
4
+ module Action
5
+ # Configuration API
6
+ #
7
+ # @since 0.2.0
8
+ #
9
+ # @see Lotus::Controller::Configuration
10
+ module Configurable
11
+ # Override Ruby's hook for modules.
12
+ # It includes configuration logic
13
+ #
14
+ # @param base [Class] the target action
15
+ #
16
+ # @since 0.2.0
17
+ # @api private
18
+ #
19
+ # @see http://www.ruby-doc.org/core-2.1.2/Module.html#method-i-included
20
+ #
21
+ # @example
22
+ # require 'lotus/controller'
23
+ #
24
+ # class Show
25
+ # include Lotus::Action
26
+ # end
27
+ #
28
+ # Show.configuration
29
+ def self.included(base)
30
+ config = Lotus::Controller::Configuration.for(base)
31
+
32
+ base.class_eval do
33
+ include Utils::ClassAttribute
34
+
35
+ class_attribute :configuration
36
+ self.configuration = config
37
+ end
38
+
39
+ config.load!(base)
40
+ end
41
+
42
+ protected
43
+ def configuration
44
+ self.class.configuration
45
+ end
46
+ end
47
+ end
48
+ end
@@ -9,20 +9,23 @@ module Lotus
9
9
  # @since 0.1.0
10
10
  #
11
11
  # @see Lotus::Action::Cookies#cookies
12
- class CookieJar < Utils::Hash
12
+ class CookieJar
13
13
  # The key that returns raw cookies from the Rack env
14
14
  #
15
15
  # @since 0.1.0
16
+ # @api private
16
17
  HTTP_HEADER = 'HTTP_COOKIE'.freeze
17
18
 
18
19
  # The key used by Rack to set the cookies as an Hash in the env
19
20
  #
20
21
  # @since 0.1.0
22
+ # @api private
21
23
  COOKIE_HASH_KEY = 'rack.request.cookie_hash'.freeze
22
24
 
23
25
  # The key used by Rack to set the cookies as a String in the env
24
26
  #
25
27
  # @since 0.1.0
28
+ # @api private
26
29
  COOKIE_STRING_KEY = 'rack.request.cookie_string'.freeze
27
30
 
28
31
  # Initialize the CookieJar
@@ -35,9 +38,7 @@ module Lotus
35
38
  # @since 0.1.0
36
39
  def initialize(env, headers)
37
40
  @_headers = headers
38
-
39
- super(extract(env))
40
- symbolize!
41
+ @cookies = Utils::Hash.new(extract(env)).symbolize!
41
42
  end
42
43
 
43
44
  # Finalize itself, by setting the proper headers to add and remove
@@ -49,7 +50,30 @@ module Lotus
49
50
  #
50
51
  # @see Lotus::Action::Cookies#finish
51
52
  def finish
52
- each {|k,v| v.nil? ? delete_cookie(k) : set_cookie(k, v) }
53
+ @cookies.each {|k,v| v.nil? ? delete_cookie(k) : set_cookie(k, v) }
54
+ end
55
+
56
+ # Returns the object associated with the given key
57
+ #
58
+ # @param key [Symbol] the key
59
+ #
60
+ # @return [Object,nil] return the associated object, if found
61
+ #
62
+ # @since 0.2.0
63
+ def [](key)
64
+ @cookies[key]
65
+ end
66
+
67
+ # Associate the given value with the given key and store them
68
+ #
69
+ # @param key [Symbol] the key
70
+ # @param value [Object] the value
71
+ #
72
+ # @return [void]
73
+ #
74
+ # @since 0.2.0
75
+ def []=(key, value)
76
+ @cookies[key] = value
53
77
  end
54
78
 
55
79
  private
@@ -6,10 +6,17 @@ module Lotus
6
6
  #
7
7
  # @see Lotus::Action::Exposable::ClassMethods#expose
8
8
  module Exposable
9
+ # Override Ruby's hook for modules.
10
+ # It includes exposures logic
11
+ #
12
+ # @param base [Class] the target action
13
+ #
14
+ # @since 0.1.0
15
+ # @api private
16
+ #
17
+ # @see http://www.ruby-doc.org/core-2.1.2/Module.html#method-i-included
9
18
  def self.included(base)
10
- base.class_eval do
11
- extend ClassMethods
12
- end
19
+ base.extend ClassMethods
13
20
  end
14
21
 
15
22
  module ClassMethods
@@ -46,8 +53,8 @@ module Lotus
46
53
  # action.exposures # => { :article => #<Article ...>, :tags => [ ... ] }
47
54
  def expose(*names)
48
55
  class_eval do
49
- attr_reader *names
50
- exposures.push *names
56
+ attr_reader( *names)
57
+ exposures.push(*names)
51
58
  end
52
59
  end
53
60
 
@@ -1,3 +1,5 @@
1
+ require 'lotus/utils/kernel'
2
+
1
3
  module Lotus
2
4
  module Action
3
5
  # Mime type API
@@ -26,21 +28,41 @@ module Lotus
26
28
  # @since 0.1.0
27
29
  DEFAULT_CONTENT_TYPE = 'application/octet-stream'.freeze
28
30
 
31
+ # Override Ruby's hook for modules.
32
+ # It includes Mime types logic
33
+ #
34
+ # @param base [Class] the target action
35
+ #
36
+ # @since 0.1.0
37
+ # @api private
38
+ #
39
+ # @see http://www.ruby-doc.org/core-2.1.2/Module.html#method-i-included
29
40
  def self.included(base)
30
- base.class_eval do
31
- extend ClassMethods
32
- end
41
+ base.extend ClassMethods
33
42
  end
34
43
 
35
44
  module ClassMethods
45
+ # @since 0.2.0
46
+ # @api private
47
+ def format_to_mime_type(format)
48
+ configuration.mime_type_for(format) ||
49
+ ::Rack::Mime.mime_type(".#{ format }", nil) or
50
+ raise Lotus::Controller::UnknownFormatError.new(format)
51
+ end
52
+
36
53
  protected
37
54
 
38
55
  # Restrict the access to the specified mime type symbols.
39
56
  #
40
57
  # @param mime_types[Array<Symbol>] one or more symbols representing mime type(s)
41
58
  #
59
+ # @raise [Lotus::Controller::UnknownFormatError] if the symbol cannot
60
+ # be converted into a mime type
61
+ #
42
62
  # @since 0.1.0
43
63
  #
64
+ # @see Lotus::Controller::Configuration#format
65
+ #
44
66
  # @example
45
67
  # require 'lotus/controller'
46
68
  #
@@ -57,19 +79,103 @@ module Lotus
57
79
  # # When called with "text/html" => 200
58
80
  # # When called with "application/json" => 200
59
81
  # # When called with "application/xml" => 406
60
- def accept(*mime_types)
61
- mime_types = mime_types.map do |mt|
62
- ::Rack::Mime.mime_type ".#{ mt }"
82
+ def accept(*formats)
83
+ mime_types = formats.map do |format|
84
+ format_to_mime_type(format)
63
85
  end
64
86
 
65
87
  before do
66
88
  unless mime_types.find {|mt| accept?(mt) }
67
- throw 406
89
+ halt 406
68
90
  end
69
91
  end
70
92
  end
71
93
  end
72
94
 
95
+ # Returns a symbol representation of the content type.
96
+ #
97
+ # The framework automatically detects the request mime type, and returns
98
+ # the corresponding format.
99
+ #
100
+ # However, if this value was explicitely set by `#format=`, it will return
101
+ # that value
102
+ #
103
+ # @return [Symbol] a symbol that corresponds to the content type
104
+ #
105
+ # @since 0.2.0
106
+ #
107
+ # @see Lotus::Action::Mime#format=
108
+ # @see Lotus::Action::Mime#content_type
109
+ #
110
+ # @example Default scenario
111
+ # require 'lotus/controller'
112
+ #
113
+ # class Show
114
+ # include Lotus::Action
115
+ #
116
+ # def call(params)
117
+ # end
118
+ # end
119
+ #
120
+ # action = Show.new
121
+ #
122
+ # _, headers, _ = action.call({ 'HTTP_ACCEPT' => 'text/html' })
123
+ # headers['Content-Type'] # => 'text/html'
124
+ # action.format # => :html
125
+ #
126
+ # @example Set value
127
+ # require 'lotus/controller'
128
+ #
129
+ # class Show
130
+ # include Lotus::Action
131
+ #
132
+ # def call(params)
133
+ # self.format = :xml
134
+ # end
135
+ # end
136
+ #
137
+ # action = Show.new
138
+ #
139
+ # _, headers, _ = action.call({ 'HTTP_ACCEPT' => 'text/html' })
140
+ # headers['Content-Type'] # => 'application/xml'
141
+ # action.format # => :xml
142
+ def format
143
+ @format ||= detect_format
144
+ end
145
+
146
+ # The content type that will be automatically set in the response.
147
+ #
148
+ # It prefers, in order:
149
+ # * Explicit set value (see #format=)
150
+ # * Weighted value from Accept
151
+ # * Default content type
152
+ #
153
+ # To override the value, use <tt>#format=</tt>
154
+ #
155
+ # @return [String] the content type from the request.
156
+ #
157
+ # @since 0.1.0
158
+ #
159
+ # @see Lotus::Action::Mime#format=
160
+ # @see Lotus::Configuration#default_format
161
+ # @see Lotus::Action::Mime#default_content_type
162
+ # @see Lotus::Action::Mime#DEFAULT_CONTENT_TYPE
163
+ #
164
+ # @example
165
+ # require 'lotus/controller'
166
+ #
167
+ # class Show
168
+ # include Lotus::Action
169
+ #
170
+ # def call(params)
171
+ # # ...
172
+ # content_type # => 'text/html'
173
+ # end
174
+ # end
175
+ def content_type
176
+ @content_type || accepts || default_content_type || DEFAULT_CONTENT_TYPE
177
+ end
178
+
73
179
  protected
74
180
  # Finalize the response by setting the current content type
75
181
  #
@@ -82,19 +188,64 @@ module Lotus
82
188
  headers.merge! CONTENT_TYPE => content_type
83
189
  end
84
190
 
85
- # Sets the given content type
191
+ # Sets the given format and corresponding content type.
192
+ #
193
+ # The framework detects the `HTTP_ACCEPT` header of the request and sets
194
+ # the proper `Content-Type` header in the response.
195
+ # Within this default scenario, `#format` returns a symbol that
196
+ # corresponds to `#content_type`.
197
+ # For instance, if a client sends an `HTTP_ACCEPT` with `text/html`,
198
+ # `#content_type` will return `text/html` and `#format` `:html`.
86
199
  #
87
- # Lotus::Action sets the proper content type automatically, this method
88
- # is designed to override that value.
200
+ # However, it's possible to override what the framework have detected.
201
+ # If a client asks for an `HTTP_ACCEPT` `*/*`, but we want to force the
202
+ # response to be a `text/html` we can use this method.
203
+ #
204
+ # When the format is set, the framework searchs for a corresponding mime
205
+ # type to be set as the `Content-Type` header of the response.
206
+ # This lookup is performed first in the configuration, and then in
207
+ # `Rack::Mime::MIME_TYPES`. If the lookup fails, it raises an error.
208
+ #
209
+ # PERFORMANCE: Because `Lotus::Controller::Configuration#formats` is
210
+ # smaller and looked up first than `Rack::Mime::MIME_TYPES`, we suggest to
211
+ # configure the most common mime types used by your application, **even
212
+ # if they are already present in that Rack constant**.
213
+ #
214
+ # @param format [#to_sym] the format
89
215
  #
90
- # @param content_type [String] the content type
91
216
  # @return [void]
92
217
  #
93
- # @since 0.1.0
218
+ # @raise [TypeError] if the format cannot be coerced into a Symbol
219
+ # @raise [Lotus::Controller::UnknownFormatError] if the format doesn't
220
+ # have a corresponding mime type
94
221
  #
222
+ # @since 0.2.0
223
+ #
224
+ # @see Lotus::Action::Mime#format
95
225
  # @see Lotus::Action::Mime#content_type
226
+ # @see Lotus::Controller::Configuration#format
96
227
  #
97
- # @example
228
+ # @example Default scenario
229
+ # require 'lotus/controller'
230
+ #
231
+ # class Show
232
+ # include Lotus::Action
233
+ #
234
+ # def call(params)
235
+ # end
236
+ # end
237
+ #
238
+ # action = Show.new
239
+ #
240
+ # _, headers, _ = action.call({ 'HTTP_ACCEPT' => '*/*' })
241
+ # headers['Content-Type'] # => 'application/octet-stream'
242
+ # action.format # => :all
243
+ #
244
+ # _, headers, _ = action.call({ 'HTTP_ACCEPT' => 'text/html' })
245
+ # headers['Content-Type'] # => 'text/html'
246
+ # action.format # => :html
247
+ #
248
+ # @example Simple usage
98
249
  # require 'lotus/controller'
99
250
  #
100
251
  # class Show
@@ -102,42 +253,54 @@ module Lotus
102
253
  #
103
254
  # def call(params)
104
255
  # # ...
105
- # self.content_type = 'application/json'
256
+ # self.format = :json
106
257
  # end
107
258
  # end
108
- def content_type=(content_type)
109
- @content_type = content_type
110
- end
111
-
112
- # The content type that will be automatically set in the response.
113
259
  #
114
- # It prefers, in order:
115
- # * Explicit set value (see #content_type=)
116
- # * Weighted value from Accept
117
- # * Default content type
260
+ # action = Show.new
118
261
  #
119
- # To override the value, use <tt>#content_type=</tt>
262
+ # _, headers, _ = action.call({ 'HTTP_ACCEPT' => '*/*' })
263
+ # headers['Content-Type'] # => 'application/json'
264
+ # action.format # => :json
120
265
  #
121
- # @return [String] the content type from the request.
266
+ # @example Unknown format
267
+ # require 'lotus/controller'
122
268
  #
123
- # @since 0.1.0
269
+ # class Show
270
+ # include Lotus::Action
124
271
  #
125
- # @see Lotus::Action::Mime#content_type=
126
- # @see Lotus::Action::Mime#DEFAULT_CONTENT_TYPE
272
+ # def call(params)
273
+ # # ...
274
+ # self.format = :unknown
275
+ # end
276
+ # end
127
277
  #
128
- # @example
278
+ # action = Show.new
279
+ # action.call({ 'HTTP_ACCEPT' => '*/*' })
280
+ # # => raise Lotus::Controller::UnknownFormatError
281
+ #
282
+ # @example Custom mime type/format
129
283
  # require 'lotus/controller'
130
284
  #
285
+ # Lotus::Controller.configure do
286
+ # format :custom, 'application/custom'
287
+ # end
288
+ #
131
289
  # class Show
132
290
  # include Lotus::Action
133
291
  #
134
292
  # def call(params)
135
293
  # # ...
136
- # content_type # => 'text/html'
294
+ # self.format = :custom
137
295
  # end
138
296
  # end
139
- def content_type
140
- @content_type || accepts || DEFAULT_CONTENT_TYPE
297
+ #
298
+ # _, headers, _ = action.call({ 'HTTP_ACCEPT' => '*/*' })
299
+ # headers['Content-Type'] # => 'application/custom'
300
+ # action.format # => :custom
301
+ def format=(format)
302
+ @format = Utils::Kernel.Symbol(format)
303
+ @content_type = self.class.format_to_mime_type(@format)
141
304
  end
142
305
 
143
306
  # Match the given mime type with the Accept header
@@ -176,15 +339,35 @@ module Lotus
176
339
  end
177
340
 
178
341
  private
342
+
343
+ # @since 0.1.0
344
+ # @api private
179
345
  def accept
180
346
  @accept ||= @_env[HTTP_ACCEPT] || DEFAULT_ACCEPT
181
347
  end
182
348
 
349
+ # @since 0.1.0
350
+ # @api private
183
351
  def accepts
184
352
  unless accept == DEFAULT_ACCEPT
185
353
  ::Rack::Utils.best_q_match(accept, ::Rack::Mime::MIME_TYPES.values)
186
354
  end
187
355
  end
356
+
357
+ # @since 0.2.0
358
+ # @api private
359
+ def default_content_type
360
+ self.class.format_to_mime_type(
361
+ configuration.default_format
362
+ ) if configuration.default_format
363
+ end
364
+
365
+ # @since 0.2.0
366
+ # @api private
367
+ def detect_format
368
+ configuration.format_for(content_type) ||
369
+ ::Rack::Mime::MIME_TYPES.key(content_type).gsub(/\A\./, '').to_sym
370
+ end
188
371
  end
189
372
  end
190
373
  end