lotus-controller 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,4 +1,6 @@
1
1
  require 'lotus/utils/hash'
2
+ require 'lotus/validations'
3
+ require 'set'
2
4
 
3
5
  module Lotus
4
6
  module Action
@@ -24,6 +26,67 @@ module Lotus
24
26
  # @since 0.1.0
25
27
  ROUTER_PARAMS = 'router.params'.freeze
26
28
 
29
+ # Whitelist and validate a parameter
30
+ #
31
+ # @param name [#to_sym] The name of the param to whitelist
32
+ #
33
+ # @raise [ArgumentError] if one the validations is unknown, or if
34
+ # the size validator is used with an object that can't be coerced to
35
+ # integer.
36
+ #
37
+ # @return void
38
+ #
39
+ # @since 0.3.0
40
+ #
41
+ # @see http://rdoc.info/gems/lotus-validations/Lotus/Validations
42
+ #
43
+ # @example Whitelisting
44
+ # require 'lotus/controller'
45
+ #
46
+ # class SignupParams < Lotus::Action::Params
47
+ # param :email
48
+ # end
49
+ #
50
+ # params = SignupParams.new({id: 23, email: 'mjb@example.com'})
51
+ #
52
+ # params[:email] # => 'mjb@example.com'
53
+ # params[:id] # => nil
54
+ #
55
+ # @example Validation
56
+ # require 'lotus/controller'
57
+ #
58
+ # class SignupParams < Lotus::Action::Params
59
+ # param :email, presence: true
60
+ # end
61
+ #
62
+ # params = SignupParams.new({})
63
+ #
64
+ # params[:email] # => nil
65
+ # params.valid? # => false
66
+ #
67
+ # @example Unknown validation
68
+ # require 'lotus/controller'
69
+ #
70
+ # class SignupParams < Lotus::Action::Params
71
+ # param :email, unknown: true # => raise ArgumentError
72
+ # end
73
+ #
74
+ # @example Wrong size validation
75
+ # require 'lotus/controller'
76
+ #
77
+ # class SignupParams < Lotus::Action::Params
78
+ # param :email, size: 'twentythree'
79
+ # end
80
+ #
81
+ # params = SignupParams.new({})
82
+ # params.valid? # => raise ArgumentError
83
+ def self.param(name, options = {})
84
+ attribute name, options
85
+ nil
86
+ end
87
+
88
+ include Lotus::Validations
89
+
27
90
  # @attr_reader env [Hash] the Rack env
28
91
  #
29
92
  # @since 0.2.0
@@ -38,11 +101,16 @@ module Lotus
38
101
  #
39
102
  # @since 0.1.0
40
103
  def initialize(env)
41
- @env = env
42
- @params = Utils::Hash.new(_extract).symbolize!
104
+ @env = env
105
+ super(_compute_params)
43
106
  freeze
44
107
  end
45
108
 
109
+ def self.defined_attributes
110
+ result = super
111
+ return result if result.to_ary.any?
112
+ end
113
+
46
114
  # Returns the object associated with the given key
47
115
  #
48
116
  # @param key [Symbol] the key
@@ -51,10 +119,26 @@ module Lotus
51
119
  #
52
120
  # @since 0.2.0
53
121
  def [](key)
54
- @params[key]
122
+ @attributes.get(key)
55
123
  end
56
124
 
125
+ # Returns the Ruby's hash
126
+ #
127
+ # @return [Hash]
128
+ #
129
+ # @since 0.3.0
130
+ def to_h
131
+ @attributes.to_h
132
+ end
133
+ alias_method :to_hash, :to_h
134
+
57
135
  private
136
+ def _compute_params
137
+ Utils::Hash.new(
138
+ _extract
139
+ ).symbolize!
140
+ end
141
+
58
142
  def _extract
59
143
  {}.tap do |result|
60
144
  if env.has_key?(RACK_INPUT)
@@ -1,15 +1,11 @@
1
+ require 'securerandom'
2
+
1
3
  module Lotus
2
4
  module Action
3
5
  # Rack integration API
4
6
  #
5
7
  # @since 0.1.0
6
8
  module Rack
7
- # The default session key for Rack
8
- #
9
- # @since 0.1.0
10
- # @api private
11
- SESSION_KEY = 'rack.session'.freeze
12
-
13
9
  # The default HTTP response code
14
10
  #
15
11
  # @since 0.1.0
@@ -22,6 +18,14 @@ module Lotus
22
18
  # @api private
23
19
  DEFAULT_RESPONSE_BODY = []
24
20
 
21
+ # The default HTTP Request ID length
22
+ #
23
+ # @since 0.3.0
24
+ # @api private
25
+ #
26
+ # @see Lotus::Action::Rack#request_id
27
+ DEFAULT_REQUEST_ID_LENGTH = 16
28
+
25
29
  # Override Ruby's hook for modules.
26
30
  # It includes basic Lotus::Action modules to the given class.
27
31
  #
@@ -56,10 +60,9 @@ module Lotus
56
60
  # @example Class Middleware
57
61
  # require 'lotus/controller'
58
62
  #
59
- # class SessionsController
60
- # include Lotus::Controller
61
- #
62
- # action 'Create' do
63
+ # module Sessions
64
+ # class Create
65
+ # include Lotus::Action
63
66
  # use OmniAuth
64
67
  #
65
68
  # def call(params)
@@ -71,10 +74,9 @@ module Lotus
71
74
  # @example Instance Middleware
72
75
  # require 'lotus/controller'
73
76
  #
74
- # class SessionsController
75
- # include Lotus::Controller
76
- #
77
- # action 'Create' do
77
+ # module Sessions
78
+ # class Create
79
+ # include Lotus::Controller
78
80
  # use XMiddleware.new('x', 123)
79
81
  #
80
82
  # def call(params)
@@ -90,31 +92,61 @@ module Lotus
90
92
  end
91
93
 
92
94
  protected
93
- # Sets the HTTP status code for the response
95
+
96
+ # Gets the headers from the response
94
97
  #
95
- # @param status [Fixnum] an HTTP status code
96
- # @return [void]
98
+ # @return [Hash] the HTTP headers from the response
97
99
  #
98
100
  # @since 0.1.0
99
101
  #
100
102
  # @example
101
103
  # require 'lotus/controller'
102
104
  #
103
- # class Create
105
+ # class Show
104
106
  # include Lotus::Action
105
107
  #
106
108
  # def call(params)
107
109
  # # ...
108
- # self.status = 201
110
+ # self.headers # => { ... }
111
+ # self.headers.merge!({'X-Custom' => 'OK'})
109
112
  # end
110
113
  # end
111
- def status=(status)
112
- @_status = status
114
+ def headers
115
+ @headers
113
116
  end
114
117
 
115
- # Sets the body of the response
118
+ # Returns a serialized Rack response (Array), according to the current
119
+ # status code, headers, and body.
116
120
  #
117
- # @param body [String] the body of the response
121
+ # @return [Array] the serialized response
122
+ #
123
+ # @since 0.1.0
124
+ # @api private
125
+ #
126
+ # @see Lotus::Action::Rack::DEFAULT_RESPONSE_CODE
127
+ # @see Lotus::Action::Rack::DEFAULT_RESPONSE_BODY
128
+ # @see Lotus::Action::Rack#status=
129
+ # @see Lotus::Action::Rack#headers
130
+ # @see Lotus::Action::Rack#body=
131
+ def response
132
+ [ @_status || DEFAULT_RESPONSE_CODE, headers, @_body || DEFAULT_RESPONSE_BODY.dup ]
133
+ end
134
+
135
+ # Calculates an unique ID for the current request
136
+ #
137
+ # @return [String] The unique ID
138
+ #
139
+ # @since 0.3.0
140
+ def request_id
141
+ # FIXME make this number configurable and document the probabilities of clashes
142
+ @request_id ||= SecureRandom.hex(DEFAULT_REQUEST_ID_LENGTH)
143
+ end
144
+
145
+ private
146
+
147
+ # Sets the HTTP status code for the response
148
+ #
149
+ # @param status [Fixnum] an HTTP status code
118
150
  # @return [void]
119
151
  #
120
152
  # @since 0.1.0
@@ -122,22 +154,22 @@ module Lotus
122
154
  # @example
123
155
  # require 'lotus/controller'
124
156
  #
125
- # class Show
157
+ # class Create
126
158
  # include Lotus::Action
127
159
  #
128
160
  # def call(params)
129
161
  # # ...
130
- # self.body = 'Hi!'
162
+ # self.status = 201
131
163
  # end
132
164
  # end
133
- def body=(body)
134
- body = Array(body) unless body.respond_to?(:each)
135
- @_body = body
165
+ def status=(status)
166
+ @_status = status
136
167
  end
137
168
 
138
- # Gets the headers from the response
169
+ # Sets the body of the response
139
170
  #
140
- # @return [Hash] the HTTP headers from the response
171
+ # @param body [String] the body of the response
172
+ # @return [void]
141
173
  #
142
174
  # @since 0.1.0
143
175
  #
@@ -149,29 +181,12 @@ module Lotus
149
181
  #
150
182
  # def call(params)
151
183
  # # ...
152
- # self.headers # => { ... }
153
- # self.headers.merge!({'X-Custom' => 'OK'})
184
+ # self.body = 'Hi!'
154
185
  # end
155
186
  # end
156
- def headers
157
- @headers
158
- end
159
-
160
- # Returns a serialized Rack response (Array), according to the current
161
- # status code, headers, and body.
162
- #
163
- # @return [Array] the serialized response
164
- #
165
- # @since 0.1.0
166
- # @api private
167
- #
168
- # @see Lotus::Action::Rack::DEFAULT_RESPONSE_CODE
169
- # @see Lotus::Action::Rack::DEFAULT_RESPONSE_BODY
170
- # @see Lotus::Action::Rack#status=
171
- # @see Lotus::Action::Rack#headers
172
- # @see Lotus::Action::Rack#body=
173
- def response
174
- [ @_status || DEFAULT_RESPONSE_CODE, headers, @_body || DEFAULT_RESPONSE_BODY.dup ]
187
+ def body=(body)
188
+ body = Array(body) unless body.respond_to?(:each)
189
+ @_body = body
175
190
  end
176
191
  end
177
192
  end
@@ -10,7 +10,7 @@ module Lotus
10
10
  # @api private
11
11
  LOCATION = 'Location'.freeze
12
12
 
13
- protected
13
+ private
14
14
 
15
15
  # Redirect to the given URL
16
16
  #
@@ -19,7 +19,7 @@ module Lotus
19
19
  #
20
20
  # @since 0.1.0
21
21
  #
22
- # @example
22
+ # @example With default status code (302)
23
23
  # require 'lotus/controller'
24
24
  #
25
25
  # class Create
@@ -30,8 +30,26 @@ module Lotus
30
30
  # redirect_to 'http://example.com/articles/23'
31
31
  # end
32
32
  # end
33
+ #
34
+ # action = Create.new
35
+ # action.call({}) # => [302, {'Location' => '/articles/23'}, '']
36
+ #
37
+ # @example With custom status code
38
+ # require 'lotus/controller'
39
+ #
40
+ # class Create
41
+ # include Lotus::Action
42
+ #
43
+ # def call(params)
44
+ # # ...
45
+ # redirect_to 'http://example.com/articles/23', status: 301
46
+ # end
47
+ # end
48
+ #
49
+ # action = Create.new
50
+ # action.call({}) # => [301, {'Location' => '/articles/23'}, '']
33
51
  def redirect_to(url, status: 302)
34
- headers.merge!(LOCATION => url)
52
+ headers[LOCATION] = url
35
53
  self.status = status
36
54
  end
37
55
  end
@@ -1,3 +1,5 @@
1
+ require 'lotus/action/flash'
2
+
1
3
  module Lotus
2
4
  module Action
3
5
  # Session API
@@ -12,6 +14,12 @@ module Lotus
12
14
  # @api private
13
15
  SESSION_KEY = 'rack.session'.freeze
14
16
 
17
+ # The key that is used by flash to transport errors
18
+ #
19
+ # @since 0.3.0
20
+ # @api private
21
+ ERRORS_KEY = :__errors
22
+
15
23
  protected
16
24
 
17
25
  # Gets the session from the request and expose it as an Hash.
@@ -44,6 +52,95 @@ module Lotus
44
52
  def session
45
53
  @_env[SESSION_KEY] ||= {}
46
54
  end
55
+
56
+ private
57
+
58
+ # Container useful to transport data with the HTTP session
59
+ #
60
+ # @return [Lotus::Action::Flash] a Flash instance
61
+ #
62
+ # @since 0.3.0
63
+ # @api private
64
+ #
65
+ # @see Lotus::Action::Flash
66
+ def flash
67
+ @flash ||= Flash.new(session, request_id)
68
+ end
69
+
70
+ # In case of validations errors, preserve those informations after a
71
+ # redirect.
72
+ #
73
+ # @return [void]
74
+ #
75
+ # @since 0.3.0
76
+ # @api private
77
+ #
78
+ # @see Lotus::Action::Redirect#redirect_to
79
+ #
80
+ # @example
81
+ # require 'lotus/controller'
82
+ #
83
+ # module Comments
84
+ # class Index
85
+ # include Lotus::Action
86
+ # include Lotus::Action::Session
87
+ #
88
+ # expose :comments
89
+ #
90
+ # def call(params)
91
+ # @comments = CommentRepository.all
92
+ # end
93
+ # end
94
+ #
95
+ # class Create
96
+ # include Lotus::Action
97
+ # include Lotus::Action::Session
98
+ #
99
+ # params do
100
+ # param :text, type: String, presence: true
101
+ # end
102
+ #
103
+ # def call(params)
104
+ # comment = Comment.new(params)
105
+ # CommentRepository.create(comment) if params.valid?
106
+ #
107
+ # redirect_to '/comments'
108
+ # end
109
+ # end
110
+ # end
111
+ #
112
+ # # The validation errors caused by Comments::Create are available
113
+ # # **after the redirect** in the context of Comments::Index.
114
+ def redirect_to(*args)
115
+ super
116
+ flash[ERRORS_KEY] = errors.to_a unless params.valid?
117
+ end
118
+
119
+ # Read errors from flash or delegate to the superclass
120
+ #
121
+ # @return [Lotus::Validations::Errors] A collection of validation errors
122
+ #
123
+ # @since 0.3.0
124
+ # @api private
125
+ #
126
+ # @see Lotus::Action::Validatable
127
+ # @see Lotus::Action::Session#flash
128
+ def errors
129
+ flash[ERRORS_KEY] || super
130
+ end
131
+
132
+ # Finalize the response
133
+ #
134
+ # @return [void]
135
+ #
136
+ # @since 0.3.0
137
+ # @api private
138
+ #
139
+ # @see Lotus::Action#finish
140
+ def finish
141
+ super
142
+ flash.clear
143
+ end
47
144
  end
48
145
  end
49
146
  end