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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +155 -0
- data/LICENSE.md +22 -0
- data/README.md +1180 -9
- data/hanami-controller.gemspec +19 -12
- data/lib/hanami-controller.rb +1 -0
- data/lib/hanami/action.rb +85 -0
- data/lib/hanami/action/cache.rb +174 -0
- data/lib/hanami/action/cache/cache_control.rb +70 -0
- data/lib/hanami/action/cache/conditional_get.rb +93 -0
- data/lib/hanami/action/cache/directives.rb +99 -0
- data/lib/hanami/action/cache/expires.rb +73 -0
- data/lib/hanami/action/callable.rb +94 -0
- data/lib/hanami/action/callbacks.rb +210 -0
- data/lib/hanami/action/configurable.rb +49 -0
- data/lib/hanami/action/cookie_jar.rb +181 -0
- data/lib/hanami/action/cookies.rb +85 -0
- data/lib/hanami/action/exposable.rb +115 -0
- data/lib/hanami/action/flash.rb +182 -0
- data/lib/hanami/action/glue.rb +66 -0
- data/lib/hanami/action/head.rb +122 -0
- data/lib/hanami/action/mime.rb +493 -0
- data/lib/hanami/action/params.rb +285 -0
- data/lib/hanami/action/rack.rb +270 -0
- data/lib/hanami/action/rack/callable.rb +47 -0
- data/lib/hanami/action/rack/file.rb +33 -0
- data/lib/hanami/action/redirect.rb +59 -0
- data/lib/hanami/action/request.rb +86 -0
- data/lib/hanami/action/session.rb +154 -0
- data/lib/hanami/action/throwable.rb +194 -0
- data/lib/hanami/action/validatable.rb +128 -0
- data/lib/hanami/controller.rb +250 -2
- data/lib/hanami/controller/configuration.rb +705 -0
- data/lib/hanami/controller/error.rb +7 -0
- data/lib/hanami/controller/version.rb +4 -1
- data/lib/hanami/http/status.rb +62 -0
- metadata +124 -16
- data/.gitignore +0 -9
- data/Gemfile +0 -4
- data/Rakefile +0 -2
- data/bin/console +0 -14
- 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
|