hanami-controller 0.0.0 → 0.6.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.
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