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