hanami-controller 0.0.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +155 -0
  3. data/LICENSE.md +22 -0
  4. data/README.md +1180 -9
  5. data/hanami-controller.gemspec +19 -12
  6. data/lib/hanami-controller.rb +1 -0
  7. data/lib/hanami/action.rb +85 -0
  8. data/lib/hanami/action/cache.rb +174 -0
  9. data/lib/hanami/action/cache/cache_control.rb +70 -0
  10. data/lib/hanami/action/cache/conditional_get.rb +93 -0
  11. data/lib/hanami/action/cache/directives.rb +99 -0
  12. data/lib/hanami/action/cache/expires.rb +73 -0
  13. data/lib/hanami/action/callable.rb +94 -0
  14. data/lib/hanami/action/callbacks.rb +210 -0
  15. data/lib/hanami/action/configurable.rb +49 -0
  16. data/lib/hanami/action/cookie_jar.rb +181 -0
  17. data/lib/hanami/action/cookies.rb +85 -0
  18. data/lib/hanami/action/exposable.rb +115 -0
  19. data/lib/hanami/action/flash.rb +182 -0
  20. data/lib/hanami/action/glue.rb +66 -0
  21. data/lib/hanami/action/head.rb +122 -0
  22. data/lib/hanami/action/mime.rb +493 -0
  23. data/lib/hanami/action/params.rb +285 -0
  24. data/lib/hanami/action/rack.rb +270 -0
  25. data/lib/hanami/action/rack/callable.rb +47 -0
  26. data/lib/hanami/action/rack/file.rb +33 -0
  27. data/lib/hanami/action/redirect.rb +59 -0
  28. data/lib/hanami/action/request.rb +86 -0
  29. data/lib/hanami/action/session.rb +154 -0
  30. data/lib/hanami/action/throwable.rb +194 -0
  31. data/lib/hanami/action/validatable.rb +128 -0
  32. data/lib/hanami/controller.rb +250 -2
  33. data/lib/hanami/controller/configuration.rb +705 -0
  34. data/lib/hanami/controller/error.rb +7 -0
  35. data/lib/hanami/controller/version.rb +4 -1
  36. data/lib/hanami/http/status.rb +62 -0
  37. metadata +124 -16
  38. data/.gitignore +0 -9
  39. data/Gemfile +0 -4
  40. data/Rakefile +0 -2
  41. data/bin/console +0 -14
  42. data/bin/setup +0 -8
@@ -0,0 +1,285 @@
1
+ require 'hanami/validations'
2
+ require 'hanami/utils/attributes'
3
+ require 'set'
4
+
5
+ module Hanami
6
+ module Action
7
+ # A set of params requested by the client
8
+ #
9
+ # It's able to extract the relevant params from a Rack env of from an Hash.
10
+ #
11
+ # There are three scenarios:
12
+ # * When used with Hanami::Router: it contains only the params from the request
13
+ # * When used standalone: it contains all the Rack env
14
+ # * Default: it returns the given hash as it is. It's useful for testing purposes.
15
+ #
16
+ # @since 0.1.0
17
+ class Params
18
+ # The key that returns raw input from the Rack env
19
+ #
20
+ # @since 0.1.0
21
+ RACK_INPUT = 'rack.input'.freeze
22
+
23
+ # The key that returns router params from the Rack env
24
+ # This is a builtin integration for Hanami::Router
25
+ #
26
+ # @since 0.1.0
27
+ ROUTER_PARAMS = 'router.params'.freeze
28
+
29
+ # CSRF params key
30
+ #
31
+ # This key is shared with <tt>hanamirb</tt> and <tt>hanami-helpers</tt>
32
+ #
33
+ # @since 0.4.4
34
+ # @api private
35
+ CSRF_TOKEN = '_csrf_token'.freeze
36
+
37
+ # Set of params that are never filtered
38
+ #
39
+ # @since 0.4.4
40
+ # @api private
41
+ DEFAULT_PARAMS = Hash[CSRF_TOKEN => true].freeze
42
+
43
+ # Separator for #get
44
+ #
45
+ # @since 0.4.0
46
+ # @api private
47
+ #
48
+ # @see Hanami::Action::Params#get
49
+ GET_SEPARATOR = '.'.freeze
50
+
51
+ # Whitelist and validate a parameter
52
+ #
53
+ # @param name [#to_sym] The name of the param to whitelist
54
+ #
55
+ # @raise [ArgumentError] if one the validations is unknown, or if
56
+ # the size validator is used with an object that can't be coerced to
57
+ # integer.
58
+ #
59
+ # @return void
60
+ #
61
+ # @since 0.3.0
62
+ #
63
+ # @see http://rdoc.info/gems/hanami-validations/Hanami/Validations
64
+ #
65
+ # @example Whitelisting
66
+ # require 'hanami/controller'
67
+ #
68
+ # class SignupParams < Hanami::Action::Params
69
+ # param :email
70
+ # end
71
+ #
72
+ # params = SignupParams.new({id: 23, email: 'mjb@example.com'})
73
+ #
74
+ # params[:email] # => 'mjb@example.com'
75
+ # params[:id] # => nil
76
+ #
77
+ # @example Validation
78
+ # require 'hanami/controller'
79
+ #
80
+ # class SignupParams < Hanami::Action::Params
81
+ # param :email, presence: true
82
+ # end
83
+ #
84
+ # params = SignupParams.new({})
85
+ #
86
+ # params[:email] # => nil
87
+ # params.valid? # => false
88
+ #
89
+ # @example Unknown validation
90
+ # require 'hanami/controller'
91
+ #
92
+ # class SignupParams < Hanami::Action::Params
93
+ # param :email, unknown: true # => raise ArgumentError
94
+ # end
95
+ #
96
+ # @example Wrong size validation
97
+ # require 'hanami/controller'
98
+ #
99
+ # class SignupParams < Hanami::Action::Params
100
+ # param :email, size: 'twentythree'
101
+ # end
102
+ #
103
+ # params = SignupParams.new({})
104
+ # params.valid? # => raise ArgumentError
105
+ def self.param(name, options = {}, &block)
106
+ attribute name, options, &block
107
+ nil
108
+ end
109
+
110
+ include Hanami::Validations
111
+
112
+ def self.whitelisting?
113
+ defined_attributes.any?
114
+ end
115
+
116
+ # Overrides the method in Hanami::Validation to build a class that
117
+ # inherits from Params rather than only Hanami::Validations.
118
+ #
119
+ # @since 0.3.2
120
+ # @api private
121
+ def self.build_validation_class(&block)
122
+ kls = Class.new(Params) do
123
+ def hanami_nested_attributes?
124
+ true
125
+ end
126
+ end
127
+ kls.class_eval(&block)
128
+ kls
129
+ end
130
+
131
+ # @attr_reader env [Hash] the Rack env
132
+ #
133
+ # @since 0.2.0
134
+ # @api private
135
+ attr_reader :env
136
+
137
+ # @attr_reader raw [Hanami::Utils::Attributes] all request's attributes
138
+ #
139
+ # @since 0.3.2
140
+ attr_reader :raw
141
+
142
+ # Initialize the params and freeze them.
143
+ #
144
+ # @param env [Hash] a Rack env or an hash of params.
145
+ #
146
+ # @return [Params]
147
+ #
148
+ # @since 0.1.0
149
+ def initialize(env)
150
+ @env = env
151
+ super(_compute_params)
152
+ # freeze
153
+ end
154
+
155
+ # Returns the object associated with the given key
156
+ #
157
+ # @param key [Symbol] the key
158
+ #
159
+ # @return [Object,nil] return the associated object, if found
160
+ #
161
+ # @since 0.2.0
162
+ def [](key)
163
+ @attributes.get(key)
164
+ end
165
+
166
+ # Get an attribute value associated with the given key.
167
+ # Nested attributes are reached with a dot notation.
168
+ #
169
+ # @param key [String] the key
170
+ #
171
+ # @return [Object,NilClass] return the associated value, if found
172
+ #
173
+ # @since 0.4.0
174
+ #
175
+ # @example
176
+ # require 'hanami/controller'
177
+ #
178
+ # module Deliveries
179
+ # class Create
180
+ # include Hanami::Action
181
+ #
182
+ # params do
183
+ # param :customer_name
184
+ # param :address do
185
+ # param :city
186
+ # end
187
+ # end
188
+ #
189
+ # def call(params)
190
+ # params.get('customer_name') # => "Luca"
191
+ # params.get('uknown') # => nil
192
+ #
193
+ # params.get('address.city') # => "Rome"
194
+ # params.get('address.unknown') # => nil
195
+ #
196
+ # params.get(nil) # => nil
197
+ # end
198
+ # end
199
+ # end
200
+ def get(key)
201
+ key, *keys = key.to_s.split(GET_SEPARATOR)
202
+ result = self[key]
203
+
204
+ Array(keys).each do |k|
205
+ break if result.nil?
206
+ result = result[k]
207
+ end
208
+
209
+ result
210
+ end
211
+
212
+ # Serialize params to Hash
213
+ #
214
+ # @return [::Hash]
215
+ #
216
+ # @since 0.3.0
217
+ def to_h
218
+ @attributes.to_h
219
+ end
220
+ alias_method :to_hash, :to_h
221
+
222
+ # Assign CSRF Token.
223
+ # This method is here for compatibility with <tt>Hanami::Validations</tt>.
224
+ #
225
+ # NOTE: When we will not support indifferent access anymore, we can probably
226
+ # remove this method.
227
+ #
228
+ # @since 0.4.4
229
+ # @api private
230
+ def _csrf_token=(value)
231
+ @attributes.set(CSRF_TOKEN, value)
232
+ end
233
+
234
+ private
235
+ # @since 0.3.1
236
+ # @api private
237
+ def _compute_params
238
+ if self.class.whitelisting?
239
+ _whitelisted_params
240
+ else
241
+ @attributes = _raw
242
+ end
243
+ end
244
+
245
+ # @since 0.3.2
246
+ # @api private
247
+ def _raw
248
+ @raw ||= Utils::Attributes.new(_params)
249
+ end
250
+
251
+ # @since 0.3.1
252
+ # @api private
253
+ def _params
254
+ {}.tap do |result|
255
+ if env.has_key?(RACK_INPUT)
256
+ result.merge! ::Rack::Request.new(env).params
257
+ result.merge! env.fetch(ROUTER_PARAMS, {})
258
+ else
259
+ result.merge! env.fetch(ROUTER_PARAMS, env)
260
+ end
261
+ end
262
+ end
263
+
264
+ # @since 0.3.1
265
+ # @api private
266
+ def _whitelisted_params
267
+ {}.tap do |result|
268
+ _raw.to_h.each do |k, v|
269
+ next unless assign_attribute?(k)
270
+
271
+ result[k] = v
272
+ end
273
+ end
274
+ end
275
+
276
+ # Override <tt>Hanami::Validations</tt> method
277
+ #
278
+ # @since 0.4.4
279
+ # @api private
280
+ def assign_attribute?(key)
281
+ DEFAULT_PARAMS[key.to_s] || super
282
+ end
283
+ end
284
+ end
285
+ end
@@ -0,0 +1,270 @@
1
+ require 'securerandom'
2
+ require 'hanami/action/request'
3
+ require 'hanami/action/rack/callable'
4
+ require 'hanami/action/rack/file'
5
+
6
+ module Hanami
7
+ module Action
8
+ # Rack integration API
9
+ #
10
+ # @since 0.1.0
11
+ module Rack
12
+ # The default HTTP response code
13
+ #
14
+ # @since 0.1.0
15
+ # @api private
16
+ DEFAULT_RESPONSE_CODE = 200
17
+
18
+ # The default Rack response body
19
+ #
20
+ # @since 0.1.0
21
+ # @api private
22
+ DEFAULT_RESPONSE_BODY = []
23
+
24
+ # The default HTTP Request ID length
25
+ #
26
+ # @since 0.3.0
27
+ # @api private
28
+ #
29
+ # @see Hanami::Action::Rack#request_id
30
+ DEFAULT_REQUEST_ID_LENGTH = 16
31
+
32
+ # The request method
33
+ #
34
+ # @since 0.3.2
35
+ # @api private
36
+ REQUEST_METHOD = 'REQUEST_METHOD'.freeze
37
+
38
+ # HEAD request
39
+ #
40
+ # @since 0.3.2
41
+ # @api private
42
+ HEAD = 'HEAD'.freeze
43
+
44
+ # Override Ruby's hook for modules.
45
+ # It includes basic Hanami::Action modules to the given class.
46
+ #
47
+ # @param base [Class] the target action
48
+ #
49
+ # @since 0.1.0
50
+ # @api private
51
+ #
52
+ # @see http://www.ruby-doc.org/core-2.1.2/Module.html#method-i-included
53
+ def self.included(base)
54
+ base.extend ClassMethods
55
+ end
56
+
57
+ module ClassMethods
58
+ # Build rack builder
59
+ #
60
+ # @return [Rack::Builder]
61
+ def rack_builder
62
+ @rack_builder ||= begin
63
+ extend Hanami::Action::Rack::Callable
64
+ rack_builder = ::Rack::Builder.new
65
+ rack_builder.run ->(env) { self.new.call(env) }
66
+ rack_builder
67
+ end
68
+ end
69
+ # Use a Rack middleware
70
+ #
71
+ # The middleware will be used as it is.
72
+ #
73
+ # At the runtime, the middleware be invoked with the raw Rack env.
74
+ #
75
+ # Multiple middlewares can be employed, just by using multiple times
76
+ # this method.
77
+ #
78
+ # @param middleware [#call] A Rack middleware
79
+ # @param args [Array] Array arguments for middleware
80
+ #
81
+ # @since 0.2.0
82
+ #
83
+ # @see Hanami::Action::Callbacks::ClassMethods#before
84
+ #
85
+ # @example Middleware
86
+ # require 'hanami/controller'
87
+ #
88
+ # module Sessions
89
+ # class Create
90
+ # include Hanami::Action
91
+ # use OmniAuth
92
+ #
93
+ # def call(params)
94
+ # # ...
95
+ # end
96
+ # end
97
+ # end
98
+ def use(middleware, *args, &block)
99
+ rack_builder.use middleware, *args, &block
100
+ end
101
+ end
102
+
103
+ protected
104
+ # Gets the headers from the response
105
+ #
106
+ # @return [Hash] the HTTP headers from the response
107
+ #
108
+ # @since 0.1.0
109
+ #
110
+ # @example
111
+ # require 'hanami/controller'
112
+ #
113
+ # class Show
114
+ # include Hanami::Action
115
+ #
116
+ # def call(params)
117
+ # # ...
118
+ # self.headers # => { ... }
119
+ # self.headers.merge!({'X-Custom' => 'OK'})
120
+ # end
121
+ # end
122
+ def headers
123
+ @headers
124
+ end
125
+
126
+ # Returns a serialized Rack response (Array), according to the current
127
+ # status code, headers, and body.
128
+ #
129
+ # @return [Array] the serialized response
130
+ #
131
+ # @since 0.1.0
132
+ # @api private
133
+ #
134
+ # @see Hanami::Action::Rack::DEFAULT_RESPONSE_CODE
135
+ # @see Hanami::Action::Rack::DEFAULT_RESPONSE_BODY
136
+ # @see Hanami::Action::Rack#status=
137
+ # @see Hanami::Action::Rack#headers
138
+ # @see Hanami::Action::Rack#body=
139
+ def response
140
+ [ @_status || DEFAULT_RESPONSE_CODE, headers, @_body || DEFAULT_RESPONSE_BODY.dup ]
141
+ end
142
+
143
+ # Calculates an unique ID for the current request
144
+ #
145
+ # @return [String] The unique ID
146
+ #
147
+ # @since 0.3.0
148
+ def request_id
149
+ # FIXME make this number configurable and document the probabilities of clashes
150
+ @request_id ||= SecureRandom.hex(DEFAULT_REQUEST_ID_LENGTH)
151
+ end
152
+
153
+ # Returns a Hanami specialized rack request
154
+ #
155
+ # @return [Hanami::Action::Request] The request
156
+ #
157
+ # @since 0.3.1
158
+ #
159
+ # @example
160
+ # require 'hanami/controller'
161
+ #
162
+ # class Create
163
+ # include Hanami::Action
164
+ #
165
+ # def call(params)
166
+ # ip = request.ip
167
+ # secure = request.ssl?
168
+ # end
169
+ # end
170
+ def request
171
+ @request ||= ::Hanami::Action::Request.new(@_env)
172
+ end
173
+
174
+ private
175
+
176
+ # Sets the HTTP status code for the response
177
+ #
178
+ # @param status [Fixnum] an HTTP status code
179
+ # @return [void]
180
+ #
181
+ # @since 0.1.0
182
+ #
183
+ # @example
184
+ # require 'hanami/controller'
185
+ #
186
+ # class Create
187
+ # include Hanami::Action
188
+ #
189
+ # def call(params)
190
+ # # ...
191
+ # self.status = 201
192
+ # end
193
+ # end
194
+ def status=(status)
195
+ @_status = status
196
+ end
197
+
198
+ # Sets the body of the response
199
+ #
200
+ # @param body [String] the body of the response
201
+ # @return [void]
202
+ #
203
+ # @since 0.1.0
204
+ #
205
+ # @example
206
+ # require 'hanami/controller'
207
+ #
208
+ # class Show
209
+ # include Hanami::Action
210
+ #
211
+ # def call(params)
212
+ # # ...
213
+ # self.body = 'Hi!'
214
+ # end
215
+ # end
216
+ def body=(body)
217
+ body = Array(body) unless body.respond_to?(:each)
218
+ @_body = body
219
+ end
220
+
221
+ # Send a file as response.
222
+ #
223
+ # It automatically handle the following cases:
224
+ #
225
+ # * <tt>Content-Type</tt> and <tt>Content-Length</tt>
226
+ # * File Not found (returns a 404)
227
+ # * Conditional GET (via <tt>If-Modified-Since</tt> header)
228
+ # * Range requests (via <tt>Range</tt> header)
229
+ #
230
+ # @param path [String, Pathname] the body of the response
231
+ # @return [void]
232
+ #
233
+ # @since 0.4.3
234
+ #
235
+ # @example
236
+ # require 'hanami/controller'
237
+ #
238
+ # class Show
239
+ # include Hanami::Action
240
+ #
241
+ # def call(params)
242
+ # # ...
243
+ # send_file Pathname.new('path/to/file')
244
+ # end
245
+ # end
246
+ def send_file(path)
247
+ result = File.new(path).call(@_env)
248
+ headers.merge!(result[1])
249
+ halt result[0], result[2]
250
+ end
251
+
252
+ # Check if the current request is a HEAD
253
+ #
254
+ # @return [TrueClass,FalseClass] the result of the check
255
+ #
256
+ # @since 0.3.2
257
+ def head?
258
+ request_method == HEAD
259
+ end
260
+
261
+ # NOTE: <tt>Hanami::Action::CSRFProtection</tt> (<tt>hanamirb</tt> gem) depends on this.
262
+ #
263
+ # @api private
264
+ # @since 0.4.4
265
+ def request_method
266
+ @_env[REQUEST_METHOD]
267
+ end
268
+ end
269
+ end
270
+ end