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