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,128 @@
|
|
1
|
+
module Hanami
|
2
|
+
module Action
|
3
|
+
module Validatable
|
4
|
+
# Defines the class name for anoymous params
|
5
|
+
#
|
6
|
+
# @api private
|
7
|
+
# @since 0.3.0
|
8
|
+
PARAMS_CLASS_NAME = 'Params'.freeze
|
9
|
+
|
10
|
+
def self.included(base)
|
11
|
+
base.class_eval do
|
12
|
+
extend ClassMethods
|
13
|
+
expose :params, :errors
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# Validatable API class methods
|
18
|
+
#
|
19
|
+
# @since 0.1.0
|
20
|
+
# @api private
|
21
|
+
module ClassMethods
|
22
|
+
# Whitelist valid parameters to be passed to Hanami::Action#call.
|
23
|
+
#
|
24
|
+
# This feature isn't mandatory, but higly recommended for security
|
25
|
+
# reasons.
|
26
|
+
#
|
27
|
+
# Because params come into your application from untrusted sources, it's
|
28
|
+
# a good practice to filter only the wanted keys that serve for your
|
29
|
+
# specific use case.
|
30
|
+
#
|
31
|
+
# Once whitelisted, the params are available as an Hash with symbols
|
32
|
+
# as keys.
|
33
|
+
#
|
34
|
+
#
|
35
|
+
#
|
36
|
+
# It accepts an anonymous block where all the params can be listed.
|
37
|
+
# It internally creates an inner class which inherits from
|
38
|
+
# Hanami::Action::Params.
|
39
|
+
#
|
40
|
+
#
|
41
|
+
# Alternatively, it accepts an concrete class that should inherit from
|
42
|
+
# Hanami::Action::Params.
|
43
|
+
#
|
44
|
+
# @param klass [Class,nil] a Hanami::Action::Params subclass
|
45
|
+
# @param blk [Proc] a block which defines the whitelisted params
|
46
|
+
#
|
47
|
+
# @return void
|
48
|
+
#
|
49
|
+
# @since 0.3.0
|
50
|
+
#
|
51
|
+
# @see Hanami::Action::Params
|
52
|
+
#
|
53
|
+
# @example Anonymous Block
|
54
|
+
# require 'hanami/controller'
|
55
|
+
#
|
56
|
+
# class Signup
|
57
|
+
# include Hanami::Action
|
58
|
+
#
|
59
|
+
# params do
|
60
|
+
# param :first_name
|
61
|
+
# param :last_name
|
62
|
+
# param :email
|
63
|
+
# end
|
64
|
+
#
|
65
|
+
# def call(params)
|
66
|
+
# puts params.class # => Signup::Params
|
67
|
+
# puts params.class.superclass # => Hanami::Action::Params
|
68
|
+
#
|
69
|
+
# puts params[:first_name] # => "Luca"
|
70
|
+
# puts params[:admin] # => nil
|
71
|
+
# end
|
72
|
+
# end
|
73
|
+
#
|
74
|
+
# @example Concrete class
|
75
|
+
# require 'hanami/controller'
|
76
|
+
#
|
77
|
+
# class SignupParams < Hanami::Action::Params
|
78
|
+
# param :first_name
|
79
|
+
# param :last_name
|
80
|
+
# param :email
|
81
|
+
# end
|
82
|
+
#
|
83
|
+
# class Signup
|
84
|
+
# include Hanami::Action
|
85
|
+
# params SignupParams
|
86
|
+
#
|
87
|
+
# def call(params)
|
88
|
+
# puts params.class # => SignupParams
|
89
|
+
# puts params.class.superclass # => Hanami::Action::Params
|
90
|
+
#
|
91
|
+
# params[:first_name] # => "Luca"
|
92
|
+
# params[:admin] # => nil
|
93
|
+
# end
|
94
|
+
# end
|
95
|
+
def params(klass = nil, &blk)
|
96
|
+
if block_given?
|
97
|
+
@params_class = const_set(PARAMS_CLASS_NAME,
|
98
|
+
Class.new(Params, &blk))
|
99
|
+
else
|
100
|
+
@params_class = klass
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# Returns the class which defines the params
|
105
|
+
#
|
106
|
+
# Returns the class which has been provided to define the
|
107
|
+
# params. By default this will be Hanami::Action::Params.
|
108
|
+
#
|
109
|
+
# @return [Class] A params class (when whitelisted) or
|
110
|
+
# Hanami::Action::Params
|
111
|
+
#
|
112
|
+
# @api private
|
113
|
+
# @since 0.3.0
|
114
|
+
def params_class
|
115
|
+
@params_class ||= params { }
|
116
|
+
end
|
117
|
+
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
# Expose validation errors
|
122
|
+
#
|
123
|
+
# @since 0.3.0
|
124
|
+
def errors
|
125
|
+
params.errors
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
data/lib/hanami/controller.rb
CHANGED
@@ -1,7 +1,255 @@
|
|
1
|
-
require
|
1
|
+
require 'hanami/utils/class_attribute'
|
2
|
+
require 'hanami/action'
|
3
|
+
require 'hanami/controller/configuration'
|
4
|
+
require 'hanami/controller/version'
|
5
|
+
require 'hanami/controller/error'
|
2
6
|
|
3
7
|
module Hanami
|
8
|
+
# A set of logically grouped actions
|
9
|
+
#
|
10
|
+
# @since 0.1.0
|
11
|
+
#
|
12
|
+
# @see Hanami::Action
|
13
|
+
#
|
14
|
+
# @example
|
15
|
+
# require 'hanami/controller'
|
16
|
+
#
|
17
|
+
# module Articles
|
18
|
+
# class Index
|
19
|
+
# include Hanami::Action
|
20
|
+
#
|
21
|
+
# # ...
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# class Show
|
25
|
+
# include Hanami::Action
|
26
|
+
#
|
27
|
+
# # ...
|
28
|
+
# end
|
29
|
+
# end
|
4
30
|
module Controller
|
5
|
-
#
|
31
|
+
# Unknown format error
|
32
|
+
#
|
33
|
+
# This error is raised when a action sets a format that it isn't recognized
|
34
|
+
# both by `Hanami::Controller::Configuration` and the list of Rack mime types
|
35
|
+
#
|
36
|
+
# @since 0.2.0
|
37
|
+
#
|
38
|
+
# @see Hanami::Action::Mime#format=
|
39
|
+
class UnknownFormatError < Hanami::Controller::Error
|
40
|
+
def initialize(format)
|
41
|
+
super("Cannot find a corresponding Mime type for '#{ format }'. Please configure it with Hanami::Controller::Configuration#format.")
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
include Utils::ClassAttribute
|
46
|
+
|
47
|
+
# Framework configuration
|
48
|
+
#
|
49
|
+
# @since 0.2.0
|
50
|
+
# @api private
|
51
|
+
class_attribute :configuration
|
52
|
+
self.configuration = Configuration.new
|
53
|
+
|
54
|
+
# Configure the framework.
|
55
|
+
# It yields the given block in the context of the configuration
|
56
|
+
#
|
57
|
+
# @param blk [Proc] the configuration block
|
58
|
+
#
|
59
|
+
# @since 0.2.0
|
60
|
+
#
|
61
|
+
# @see Hanami::Controller::Configuration
|
62
|
+
#
|
63
|
+
# @example
|
64
|
+
# require 'hanami/controller'
|
65
|
+
#
|
66
|
+
# Hanami::Controller.configure do
|
67
|
+
# handle_exceptions false
|
68
|
+
# end
|
69
|
+
def self.configure(&blk)
|
70
|
+
configuration.instance_eval(&blk)
|
71
|
+
end
|
72
|
+
|
73
|
+
# Duplicate Hanami::Controller in order to create a new separated instance
|
74
|
+
# of the framework.
|
75
|
+
#
|
76
|
+
# The new instance of the framework will be completely decoupled from the
|
77
|
+
# original. It will inherit the configuration, but all the changes that
|
78
|
+
# happen after the duplication, won't be reflected on the other copies.
|
79
|
+
#
|
80
|
+
# @return [Module] a copy of Hanami::Controller
|
81
|
+
#
|
82
|
+
# @since 0.2.0
|
83
|
+
# @api private
|
84
|
+
#
|
85
|
+
# @example Basic usage
|
86
|
+
# require 'hanami/controller'
|
87
|
+
#
|
88
|
+
# module MyApp
|
89
|
+
# Controller = Hanami::Controller.dupe
|
90
|
+
# end
|
91
|
+
#
|
92
|
+
# MyApp::Controller == Hanami::Controller # => false
|
93
|
+
#
|
94
|
+
# MyApp::Controller.configuration ==
|
95
|
+
# Hanami::Controller.configuration # => false
|
96
|
+
#
|
97
|
+
# @example Inheriting configuration
|
98
|
+
# require 'hanami/controller'
|
99
|
+
#
|
100
|
+
# Hanami::Controller.configure do
|
101
|
+
# handle_exceptions false
|
102
|
+
# end
|
103
|
+
#
|
104
|
+
# module MyApp
|
105
|
+
# Controller = Hanami::Controller.dupe
|
106
|
+
# end
|
107
|
+
#
|
108
|
+
# module MyApi
|
109
|
+
# Controller = Hanami::Controller.dupe
|
110
|
+
# Controller.configure do
|
111
|
+
# handle_exceptions true
|
112
|
+
# end
|
113
|
+
# end
|
114
|
+
#
|
115
|
+
# Hanami::Controller.configuration.handle_exceptions # => false
|
116
|
+
# MyApp::Controller.configuration.handle_exceptions # => false
|
117
|
+
# MyApi::Controller.configuration.handle_exceptions # => true
|
118
|
+
def self.dupe
|
119
|
+
dup.tap do |duplicated|
|
120
|
+
duplicated.configuration = configuration.duplicate
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
# Duplicate the framework and generate modules for the target application
|
125
|
+
#
|
126
|
+
# @param mod [Module] the Ruby namespace of the application
|
127
|
+
# @param controllers [String] the optional namespace where the application's
|
128
|
+
# controllers will live
|
129
|
+
# @param blk [Proc] an optional block to configure the framework
|
130
|
+
#
|
131
|
+
# @return [Module] a copy of Hanami::Controller
|
132
|
+
#
|
133
|
+
# @since 0.2.0
|
134
|
+
#
|
135
|
+
# @see Hanami::Controller#dupe
|
136
|
+
# @see Hanami::Controller::Configuration
|
137
|
+
# @see Hanami::Controller::Configuration#action_module
|
138
|
+
#
|
139
|
+
# @example Basic usage
|
140
|
+
# require 'hanami/controller'
|
141
|
+
#
|
142
|
+
# module MyApp
|
143
|
+
# Controller = Hanami::Controller.duplicate(self)
|
144
|
+
# end
|
145
|
+
#
|
146
|
+
# # It will:
|
147
|
+
# #
|
148
|
+
# # 1. Generate MyApp::Controller
|
149
|
+
# # 2. Generate MyApp::Action
|
150
|
+
# # 3. Generate MyApp::Controllers
|
151
|
+
# # 4. Configure MyApp::Action as the default module for actions
|
152
|
+
#
|
153
|
+
# module MyApp::Controllers::Dashboard
|
154
|
+
# include MyApp::Controller
|
155
|
+
#
|
156
|
+
# action 'Index' do # this will inject MyApp::Action
|
157
|
+
# def call(params)
|
158
|
+
# # ...
|
159
|
+
# end
|
160
|
+
# end
|
161
|
+
# end
|
162
|
+
#
|
163
|
+
# @example Compare code
|
164
|
+
# require 'hanami/controller'
|
165
|
+
#
|
166
|
+
# module MyApp
|
167
|
+
# Controller = Hanami::Controller.duplicate(self) do
|
168
|
+
# # ...
|
169
|
+
# end
|
170
|
+
# end
|
171
|
+
#
|
172
|
+
# # it's equivalent to:
|
173
|
+
#
|
174
|
+
# module MyApp
|
175
|
+
# Controller = Hanami::Controller.dupe
|
176
|
+
# Action = Hanami::Action.dup
|
177
|
+
#
|
178
|
+
# module Controllers
|
179
|
+
# end
|
180
|
+
#
|
181
|
+
# Controller.configure do
|
182
|
+
# action_module MyApp::Action
|
183
|
+
# end
|
184
|
+
#
|
185
|
+
# Controller.configure do
|
186
|
+
# # ...
|
187
|
+
# end
|
188
|
+
# end
|
189
|
+
#
|
190
|
+
# @example Custom controllers module
|
191
|
+
# require 'hanami/controller'
|
192
|
+
#
|
193
|
+
# module MyApp
|
194
|
+
# Controller = Hanami::Controller.duplicate(self, 'Ctrls')
|
195
|
+
# end
|
196
|
+
#
|
197
|
+
# defined?(MyApp::Controllers) # => nil
|
198
|
+
# defined?(MyApp::Ctrls) # => "constant"
|
199
|
+
#
|
200
|
+
# # Developers can namespace controllers under Ctrls
|
201
|
+
# module MyApp::Ctrls::Dashboard
|
202
|
+
# # ...
|
203
|
+
# end
|
204
|
+
#
|
205
|
+
# @example Nil controllers module
|
206
|
+
# require 'hanami/controller'
|
207
|
+
#
|
208
|
+
# module MyApp
|
209
|
+
# Controller = Hanami::Controller.duplicate(self, nil)
|
210
|
+
# end
|
211
|
+
#
|
212
|
+
# defined?(MyApp::Controllers) # => nil
|
213
|
+
#
|
214
|
+
# # Developers can namespace controllers under MyApp
|
215
|
+
# module MyApp::DashboardController
|
216
|
+
# # ...
|
217
|
+
# end
|
218
|
+
#
|
219
|
+
# @example Block usage
|
220
|
+
# require 'hanami/controller'
|
221
|
+
#
|
222
|
+
# module MyApp
|
223
|
+
# Controller = Hanami::Controller.duplicate(self) do
|
224
|
+
# handle_exceptions false
|
225
|
+
# end
|
226
|
+
# end
|
227
|
+
#
|
228
|
+
# Hanami::Controller.configuration.handle_exceptions # => true
|
229
|
+
# MyApp::Controller.configuration.handle_exceptions # => false
|
230
|
+
def self.duplicate(mod, controllers = 'Controllers', &blk)
|
231
|
+
dupe.tap do |duplicated|
|
232
|
+
mod.module_eval %{ module #{ controllers }; end } if controllers
|
233
|
+
mod.module_eval %{ Action = Hanami::Action.dup }
|
234
|
+
|
235
|
+
duplicated.module_eval %{
|
236
|
+
configure do
|
237
|
+
action_module #{ mod }::Action
|
238
|
+
end
|
239
|
+
}
|
240
|
+
|
241
|
+
duplicated.configure(&blk) if block_given?
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
# Framework loading entry point
|
246
|
+
#
|
247
|
+
# @return [void]
|
248
|
+
#
|
249
|
+
# @since 0.3.0
|
250
|
+
def self.load!
|
251
|
+
configuration.load!
|
252
|
+
end
|
6
253
|
end
|
7
254
|
end
|
255
|
+
|
@@ -0,0 +1,705 @@
|
|
1
|
+
require 'hanami/utils/class'
|
2
|
+
require 'hanami/utils/kernel'
|
3
|
+
require 'hanami/utils/string'
|
4
|
+
|
5
|
+
module Hanami
|
6
|
+
module Controller
|
7
|
+
# Configuration for the framework, controllers and actions.
|
8
|
+
#
|
9
|
+
# Hanami::Controller has its own global configuration that can be manipulated
|
10
|
+
# via `Hanami::Controller.configure`.
|
11
|
+
#
|
12
|
+
# Every time that `Hanami::Controller` and `Hanami::Action` are included, that
|
13
|
+
# global configuration is being copied to the recipient. The copy will
|
14
|
+
# inherit all the settings from the original, but all the subsequent changes
|
15
|
+
# aren't reflected from the parent to the children, and viceversa.
|
16
|
+
#
|
17
|
+
# This architecture allows to have a global configuration that capture the
|
18
|
+
# most common cases for an application, and let controllers and single
|
19
|
+
# actions to specify exceptions.
|
20
|
+
#
|
21
|
+
# @since 0.2.0
|
22
|
+
class Configuration
|
23
|
+
# Default HTTP code for server side errors
|
24
|
+
#
|
25
|
+
# @since 0.2.0
|
26
|
+
# @api private
|
27
|
+
DEFAULT_ERROR_CODE = 500
|
28
|
+
|
29
|
+
# Default Mime type to format mapping
|
30
|
+
#
|
31
|
+
# @since 0.2.0
|
32
|
+
# @api private
|
33
|
+
DEFAULT_FORMATS = {
|
34
|
+
'application/octet-stream' => :all,
|
35
|
+
'*/*' => :all,
|
36
|
+
'text/html' => :html
|
37
|
+
}.freeze
|
38
|
+
|
39
|
+
# Return a copy of the configuration of the framework instance associated
|
40
|
+
# with the given class.
|
41
|
+
#
|
42
|
+
# When multiple instances of Hanami::Controller are used in the same
|
43
|
+
# application, we want to make sure that a controller or an action will
|
44
|
+
# receive the expected configuration.
|
45
|
+
#
|
46
|
+
# @param base [Class, Module] a controller or an action
|
47
|
+
#
|
48
|
+
# @return [Hanami::Controller::Configuration] the configuration associated
|
49
|
+
# to the given class.
|
50
|
+
#
|
51
|
+
# @since 0.2.0
|
52
|
+
# @api private
|
53
|
+
#
|
54
|
+
# @example Direct usage of the framework
|
55
|
+
# require 'hanami/controller'
|
56
|
+
#
|
57
|
+
# class Show
|
58
|
+
# include Hanami::Action
|
59
|
+
# end
|
60
|
+
#
|
61
|
+
# Hanami::Controller::Configuration.for(Show)
|
62
|
+
# # => will duplicate from Hanami::Controller
|
63
|
+
#
|
64
|
+
# @example Multiple instances of the framework
|
65
|
+
# require 'hanami/controller'
|
66
|
+
#
|
67
|
+
# module MyApp
|
68
|
+
# Controller = Hanami::Controller.duplicate(self)
|
69
|
+
#
|
70
|
+
# module Controllers::Dashboard
|
71
|
+
# class Index
|
72
|
+
# include MyApp::Action
|
73
|
+
#
|
74
|
+
# def call(params)
|
75
|
+
# # ...
|
76
|
+
# end
|
77
|
+
# end
|
78
|
+
# end
|
79
|
+
# end
|
80
|
+
#
|
81
|
+
# class Show
|
82
|
+
# include Hanami::Action
|
83
|
+
# end
|
84
|
+
#
|
85
|
+
# Hanami::Controller::Configuration.for(Show)
|
86
|
+
# # => will duplicate from Hanami::Controller
|
87
|
+
#
|
88
|
+
# Hanami::Controller::Configuration.for(MyApp::Controllers::Dashboard)
|
89
|
+
# # => will duplicate from MyApp::Controller
|
90
|
+
def self.for(base)
|
91
|
+
namespace = Utils::String.new(base).namespace
|
92
|
+
framework = Utils::Class.load_from_pattern!("(#{namespace}|Hanami)::Controller")
|
93
|
+
framework.configuration.duplicate
|
94
|
+
end
|
95
|
+
|
96
|
+
# Initialize a configuration instance
|
97
|
+
#
|
98
|
+
# @return [Hanami::Controller::Configuration] a new configuration's
|
99
|
+
# instance
|
100
|
+
#
|
101
|
+
# @since 0.2.0
|
102
|
+
def initialize
|
103
|
+
reset!
|
104
|
+
end
|
105
|
+
|
106
|
+
# @attr_writer handle_exceptions [TrueClass,FalseClass] Handle exceptions
|
107
|
+
# with an HTTP status or leave them uncaught
|
108
|
+
#
|
109
|
+
# @since 0.2.0
|
110
|
+
#
|
111
|
+
# @return void
|
112
|
+
#
|
113
|
+
# @see Hanami::Controller::Configuration#handle_exceptions
|
114
|
+
attr_writer :handle_exceptions
|
115
|
+
|
116
|
+
# Handle exceptions with an HTTP status or let them uncaught
|
117
|
+
#
|
118
|
+
# If this value is set to `true`, the configured exceptions will return
|
119
|
+
# the specified HTTP status, the rest of them with `500`.
|
120
|
+
#
|
121
|
+
# If this value is set to `false`, the exceptions won't be caught.
|
122
|
+
#
|
123
|
+
# This is part of a DSL, for this reason when this method is called with
|
124
|
+
# an argument, it will set the corresponding instance variable. When
|
125
|
+
# called without, it will return the already set value, or the default.
|
126
|
+
#
|
127
|
+
# @overload handle_exceptions(value)
|
128
|
+
# Sets the given value
|
129
|
+
# @param value [TrueClass, FalseClass] true or false, default to true
|
130
|
+
#
|
131
|
+
# @overload handle_exceptions
|
132
|
+
# Gets the value
|
133
|
+
# @return [TrueClass, FalseClass]
|
134
|
+
#
|
135
|
+
# @since 0.2.0
|
136
|
+
#
|
137
|
+
# @see Hanami::Controller::Configuration#handle_exception
|
138
|
+
# @see Hanami::Controller#configure
|
139
|
+
# @see Hanami::Action::Throwable
|
140
|
+
# @see http://httpstatus.es/500
|
141
|
+
#
|
142
|
+
# @example Getting the value
|
143
|
+
# require 'hanami/controller'
|
144
|
+
#
|
145
|
+
# Hanami::Controller.configuration.handle_exceptions # => true
|
146
|
+
#
|
147
|
+
# @example Setting the value
|
148
|
+
# require 'hanami/controller'
|
149
|
+
#
|
150
|
+
# Hanami::Controller.configure do
|
151
|
+
# handle_exceptions false
|
152
|
+
# end
|
153
|
+
def handle_exceptions(value = nil)
|
154
|
+
if value.nil?
|
155
|
+
@handle_exceptions
|
156
|
+
else
|
157
|
+
@handle_exceptions = value
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
# Specify how to handle an exception with an HTTP status
|
162
|
+
#
|
163
|
+
# Raised exceptions will return the configured HTTP status, only if
|
164
|
+
# `handled_exceptions` is set on `true`.
|
165
|
+
#
|
166
|
+
# @param exception [Hash] the exception class must be the key and the HTTP
|
167
|
+
# status the value
|
168
|
+
#
|
169
|
+
# @since 0.2.0
|
170
|
+
#
|
171
|
+
# @see Hanami::Controller::Configuration#handle_exceptions
|
172
|
+
# @see Hanami::Controller#configure
|
173
|
+
# @see Hanami::Action::Throwable
|
174
|
+
#
|
175
|
+
# @example
|
176
|
+
# require 'hanami/controller'
|
177
|
+
#
|
178
|
+
# Hanami::Controller.configure do
|
179
|
+
# handle_exception ArgumentError => 400
|
180
|
+
# end
|
181
|
+
def handle_exception(exception)
|
182
|
+
@handled_exceptions.merge!(exception)
|
183
|
+
_sort_handled_exceptions!
|
184
|
+
end
|
185
|
+
|
186
|
+
# Return a callable handler for the given exception
|
187
|
+
#
|
188
|
+
# @param exception [Exception] an exception
|
189
|
+
#
|
190
|
+
# @since 0.3.0
|
191
|
+
# @api private
|
192
|
+
#
|
193
|
+
# @see Hanami::Controller::Configuration#handle_exception
|
194
|
+
def exception_handler(exception)
|
195
|
+
handler = nil
|
196
|
+
|
197
|
+
@handled_exceptions.each do |exception_class, h|
|
198
|
+
if exception.kind_of?(exception_class)
|
199
|
+
handler = h
|
200
|
+
break
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
handler || DEFAULT_ERROR_CODE
|
205
|
+
end
|
206
|
+
|
207
|
+
# Check if the given exception is handled.
|
208
|
+
#
|
209
|
+
# @param exception [Exception] an exception
|
210
|
+
#
|
211
|
+
# @since 0.3.2
|
212
|
+
# @api private
|
213
|
+
#
|
214
|
+
# @see Hanami::Controller::Configuration#handle_exception
|
215
|
+
def handled_exception?(exception)
|
216
|
+
handled_exceptions &&
|
217
|
+
!!@handled_exceptions.fetch(exception.class) { false }
|
218
|
+
end
|
219
|
+
|
220
|
+
# Specify which is the default action module to be included when we use
|
221
|
+
# the `Hanami::Controller.action` method.
|
222
|
+
#
|
223
|
+
# This setting is useful when we use multiple instances of the framework
|
224
|
+
# in the same process, so we want to ensure that the actions will include
|
225
|
+
# `MyApp::Action`, rather than `AnotherApp::Action`.
|
226
|
+
#
|
227
|
+
# If not set, the default value is `Hanami::Action`
|
228
|
+
#
|
229
|
+
# This is part of a DSL, for this reason when this method is called with
|
230
|
+
# an argument, it will set the corresponding instance variable. When
|
231
|
+
# called without, it will return the already set value, or the default.
|
232
|
+
#
|
233
|
+
# @overload action_module(value)
|
234
|
+
# Sets the given value
|
235
|
+
# @param value [Module] the module to be included in all the actions
|
236
|
+
#
|
237
|
+
# @overload action_module
|
238
|
+
# Gets the value
|
239
|
+
# @return [Module]
|
240
|
+
#
|
241
|
+
# @since 0.2.0
|
242
|
+
#
|
243
|
+
# @see Hanami::Controller::Dsl#action
|
244
|
+
# @see Hanami::Controller#duplicate
|
245
|
+
#
|
246
|
+
# @example Getting the value
|
247
|
+
# require 'hanami/controller'
|
248
|
+
#
|
249
|
+
# Hanami::Controller.configuration.action_module # => Hanami::Action
|
250
|
+
#
|
251
|
+
# @example Setting the value
|
252
|
+
# require 'hanami/controller'
|
253
|
+
#
|
254
|
+
# module MyAction
|
255
|
+
# end
|
256
|
+
#
|
257
|
+
# Hanami::Controller.configure do
|
258
|
+
# action_module MyAction
|
259
|
+
# end
|
260
|
+
#
|
261
|
+
# module Dashboard
|
262
|
+
# # It includes MyAction, instead of Hanami::Action
|
263
|
+
# class Index
|
264
|
+
# include MyAction
|
265
|
+
#
|
266
|
+
# def call(params)
|
267
|
+
# # ...
|
268
|
+
# end
|
269
|
+
# end
|
270
|
+
# end
|
271
|
+
#
|
272
|
+
# @example Duplicated framework
|
273
|
+
# require 'hanami/controller'
|
274
|
+
#
|
275
|
+
# module MyApp
|
276
|
+
# Controller = Hanami::Controller.duplicate(self)
|
277
|
+
#
|
278
|
+
# module Controllers::Dashboard
|
279
|
+
# include MyApp::Controller
|
280
|
+
#
|
281
|
+
# # It includes MyApp::Action, instead of Hanami::Action
|
282
|
+
# class Index
|
283
|
+
# include MyApp::Action
|
284
|
+
#
|
285
|
+
# def call(params)
|
286
|
+
# # ...
|
287
|
+
# end
|
288
|
+
# end
|
289
|
+
# end
|
290
|
+
# end
|
291
|
+
def action_module(value = nil)
|
292
|
+
if value.nil?
|
293
|
+
@action_module
|
294
|
+
else
|
295
|
+
@action_module = value
|
296
|
+
end
|
297
|
+
end
|
298
|
+
|
299
|
+
# Configure the logic to be executed when Hanami::Action is included
|
300
|
+
# This is useful to DRY code by having a single place where to configure
|
301
|
+
# shared behaviors like authentication, sessions, cookies etc.
|
302
|
+
#
|
303
|
+
# This method can be called multiple times.
|
304
|
+
#
|
305
|
+
# @param blk [Proc] the code block
|
306
|
+
#
|
307
|
+
# @return [void]
|
308
|
+
#
|
309
|
+
# @raise [ArgumentError] if called without passing a block
|
310
|
+
#
|
311
|
+
# @since 0.3.0
|
312
|
+
#
|
313
|
+
# @see Hanami::Controller.configure
|
314
|
+
# @see Hanami::Controller.duplicate
|
315
|
+
#
|
316
|
+
# @example Configure shared logic.
|
317
|
+
# require 'hanami/controller'
|
318
|
+
#
|
319
|
+
# Hanami::Controller.configure do
|
320
|
+
# prepare do
|
321
|
+
# include Hanami::Action::Sessions
|
322
|
+
# include MyAuthentication
|
323
|
+
# use SomeMiddleWare
|
324
|
+
#
|
325
|
+
# before { authenticate! }
|
326
|
+
# end
|
327
|
+
# end
|
328
|
+
#
|
329
|
+
# module Dashboard
|
330
|
+
# class Index
|
331
|
+
# # When Hanami::Action is included, it will:
|
332
|
+
# # * Include `Hanami::Action::Session` and `MyAuthentication`
|
333
|
+
# # * Configure to use `SomeMiddleWare`
|
334
|
+
# # * Configure a `before` callback that triggers `#authenticate!`
|
335
|
+
# include Hanami::Action
|
336
|
+
#
|
337
|
+
# def call(params)
|
338
|
+
# # ...
|
339
|
+
# end
|
340
|
+
# end
|
341
|
+
# end
|
342
|
+
def prepare(&blk)
|
343
|
+
if block_given?
|
344
|
+
@modules.push(blk)
|
345
|
+
else
|
346
|
+
raise ArgumentError.new('Please provide a block')
|
347
|
+
end
|
348
|
+
end
|
349
|
+
|
350
|
+
# Register a format
|
351
|
+
#
|
352
|
+
# @param hash [Hash] the symbol format must be the key and the mime type
|
353
|
+
# string must be the value of the hash
|
354
|
+
#
|
355
|
+
# @since 0.2.0
|
356
|
+
#
|
357
|
+
# @see Hanami::Action::Mime
|
358
|
+
#
|
359
|
+
# @example
|
360
|
+
# require 'hanami/controller'
|
361
|
+
#
|
362
|
+
# Hanami::Controller.configure do
|
363
|
+
# format custom: 'application/custom'
|
364
|
+
# end
|
365
|
+
#
|
366
|
+
# module Articles
|
367
|
+
# class Index
|
368
|
+
# include Hanami::Action
|
369
|
+
#
|
370
|
+
# def call(params)
|
371
|
+
# # ...
|
372
|
+
# end
|
373
|
+
# end
|
374
|
+
#
|
375
|
+
# class Show
|
376
|
+
# include Hanami::Action
|
377
|
+
#
|
378
|
+
# def call(params)
|
379
|
+
# # ...
|
380
|
+
# self.format = :custom
|
381
|
+
# end
|
382
|
+
# end
|
383
|
+
# end
|
384
|
+
#
|
385
|
+
# action = Articles::Index.new
|
386
|
+
#
|
387
|
+
# action.call({ 'HTTP_ACCEPT' => 'text/html' })
|
388
|
+
# # => Content-Type "text/html"
|
389
|
+
# action.format # => :html
|
390
|
+
#
|
391
|
+
# action.call({ 'HTTP_ACCEPT' => 'application/custom' })
|
392
|
+
# # => Content-Type "application/custom"
|
393
|
+
# action.format # => :custom
|
394
|
+
#
|
395
|
+
#
|
396
|
+
#
|
397
|
+
# action = Articles::Show.new
|
398
|
+
#
|
399
|
+
# action.call({ 'HTTP_ACCEPT' => 'text/html' })
|
400
|
+
# # => Content-Type "application/custom"
|
401
|
+
# action.format # => :custom
|
402
|
+
def format(hash)
|
403
|
+
symbol, mime_type = *Utils::Kernel.Array(hash)
|
404
|
+
|
405
|
+
@formats.merge! Utils::Kernel.String(mime_type) =>
|
406
|
+
Utils::Kernel.Symbol(symbol)
|
407
|
+
end
|
408
|
+
|
409
|
+
# Set a format as default fallback for all the requests without a strict
|
410
|
+
# requirement for the mime type.
|
411
|
+
#
|
412
|
+
# The given format must be coercible to a symbol, and be a valid mime type
|
413
|
+
# alias. If it isn't, at the runtime the framework will raise a
|
414
|
+
# `Hanami::Controller::UnknownFormatError`.
|
415
|
+
#
|
416
|
+
# By default this value is nil.
|
417
|
+
#
|
418
|
+
# This is part of a DSL, for this reason when this method is called with
|
419
|
+
# an argument, it will set the corresponding instance variable. When
|
420
|
+
# called without, it will return the already set value, or the default.
|
421
|
+
#
|
422
|
+
# @overload default_request_format(format)
|
423
|
+
# Sets the given value
|
424
|
+
# @param format [#to_sym] the symbol format
|
425
|
+
# @raise [TypeError] if it cannot be coerced to a symbol
|
426
|
+
#
|
427
|
+
# @overload default_request_format
|
428
|
+
# Gets the value
|
429
|
+
# @return [Symbol,nil]
|
430
|
+
#
|
431
|
+
# @since 0.5.0
|
432
|
+
#
|
433
|
+
# @see Hanami::Action::Mime
|
434
|
+
#
|
435
|
+
# @example Getting the value
|
436
|
+
# require 'hanami/controller'
|
437
|
+
#
|
438
|
+
# Hanami::Controller.configuration.default_request_format # => nil
|
439
|
+
#
|
440
|
+
# @example Setting the value
|
441
|
+
# require 'hanami/controller'
|
442
|
+
#
|
443
|
+
# Hanami::Controller.configure do
|
444
|
+
# default_request_format :html
|
445
|
+
# end
|
446
|
+
def default_request_format(format = nil)
|
447
|
+
if format
|
448
|
+
@default_request_format = Utils::Kernel.Symbol(format)
|
449
|
+
else
|
450
|
+
@default_request_format
|
451
|
+
end
|
452
|
+
end
|
453
|
+
|
454
|
+
# Set a format to be used for all responses regardless of the request type.
|
455
|
+
#
|
456
|
+
# The given format must be coercible to a symbol, and be a valid mime type
|
457
|
+
# alias. If it isn't, at the runtime the framework will raise a
|
458
|
+
# `Hanami::Controller::UnknownFormatError`.
|
459
|
+
#
|
460
|
+
# By default this value is nil.
|
461
|
+
#
|
462
|
+
# This is part of a DSL, for this reason when this method is called with
|
463
|
+
# an argument, it will set the corresponding instance variable. When
|
464
|
+
# called without, it will return the already set value, or the default.
|
465
|
+
#
|
466
|
+
# @overload default_response_format(format)
|
467
|
+
# Sets the given value
|
468
|
+
# @param format [#to_sym] the symbol format
|
469
|
+
# @raise [TypeError] if it cannot be coerced to a symbol
|
470
|
+
#
|
471
|
+
# @overload default_response_format
|
472
|
+
# Gets the value
|
473
|
+
# @return [Symbol,nil]
|
474
|
+
#
|
475
|
+
# @since 0.5.0
|
476
|
+
#
|
477
|
+
# @see Hanami::Action::Mime
|
478
|
+
#
|
479
|
+
# @example Getting the value
|
480
|
+
# require 'hanami/controller'
|
481
|
+
#
|
482
|
+
# Hanami::Controller.configuration.default_response_format # => nil
|
483
|
+
#
|
484
|
+
# @example Setting the value
|
485
|
+
# require 'hanami/controller'
|
486
|
+
#
|
487
|
+
# Hanami::Controller.configure do
|
488
|
+
# default_response_format :json
|
489
|
+
# end
|
490
|
+
def default_response_format(format = nil)
|
491
|
+
if format
|
492
|
+
@default_response_format = Utils::Kernel.Symbol(format)
|
493
|
+
else
|
494
|
+
@default_response_format
|
495
|
+
end
|
496
|
+
end
|
497
|
+
|
498
|
+
# Set a charset as default fallback for all the requests without a strict
|
499
|
+
# requirement for the charset.
|
500
|
+
#
|
501
|
+
# By default this value is nil.
|
502
|
+
#
|
503
|
+
# @since 0.3.0
|
504
|
+
#
|
505
|
+
# @see Hanami::Action::Mime
|
506
|
+
#
|
507
|
+
# @example Getting the value
|
508
|
+
# require 'hanami/controller'
|
509
|
+
#
|
510
|
+
# Hanami::Controller.configuration.default_charset # => nil
|
511
|
+
#
|
512
|
+
# @example Setting the value
|
513
|
+
# require 'hanami/controller'
|
514
|
+
#
|
515
|
+
# Hanami::Controller.configure do
|
516
|
+
# default_charset 'koi8-r'
|
517
|
+
# end
|
518
|
+
def default_charset(charset = nil)
|
519
|
+
if charset
|
520
|
+
@default_charset = charset
|
521
|
+
else
|
522
|
+
@default_charset
|
523
|
+
end
|
524
|
+
end
|
525
|
+
|
526
|
+
# Set default headers for all responses
|
527
|
+
#
|
528
|
+
# By default this value is an empty hash.
|
529
|
+
#
|
530
|
+
# @since 0.4.0
|
531
|
+
#
|
532
|
+
# @example Getting the value
|
533
|
+
# require 'hanami/controller'
|
534
|
+
#
|
535
|
+
# Hanami::Controller.configuration.default_headers # => {}
|
536
|
+
#
|
537
|
+
# @example Setting the value
|
538
|
+
# require 'hanami/controller'
|
539
|
+
#
|
540
|
+
# Hanami::Controller.configure do
|
541
|
+
# default_headers({
|
542
|
+
# 'X-Frame-Options' => 'DENY'
|
543
|
+
# })
|
544
|
+
# end
|
545
|
+
def default_headers(headers = nil)
|
546
|
+
if headers
|
547
|
+
@default_headers.merge!(
|
548
|
+
headers.reject {|_,v| v.nil? }
|
549
|
+
)
|
550
|
+
else
|
551
|
+
@default_headers
|
552
|
+
end
|
553
|
+
end
|
554
|
+
|
555
|
+
# Set default cookies options for all responses
|
556
|
+
#
|
557
|
+
# By default this value is an empty hash.
|
558
|
+
#
|
559
|
+
# @since 0.4.0
|
560
|
+
#
|
561
|
+
# @example Getting the value
|
562
|
+
# require 'hanami/controller'
|
563
|
+
#
|
564
|
+
# Hanami::Controller.configuration.cookies # => {}
|
565
|
+
#
|
566
|
+
# @example Setting the value
|
567
|
+
# require 'hanami/controller'
|
568
|
+
#
|
569
|
+
# Hanami::Controller.configure do
|
570
|
+
# cookies({
|
571
|
+
# domain: 'hanamirb.org',
|
572
|
+
# path: '/controller',
|
573
|
+
# secure: true,
|
574
|
+
# httponly: true
|
575
|
+
# })
|
576
|
+
# end
|
577
|
+
def cookies(options = nil)
|
578
|
+
if options
|
579
|
+
@cookies.merge!(
|
580
|
+
options.reject { |_, v| v.nil? }
|
581
|
+
)
|
582
|
+
else
|
583
|
+
@cookies
|
584
|
+
end
|
585
|
+
end
|
586
|
+
|
587
|
+
# Returns a format for the given mime type
|
588
|
+
#
|
589
|
+
# @param mime_type [#to_s,#to_str] A mime type
|
590
|
+
#
|
591
|
+
# @return [Symbol,nil] the corresponding format, if present
|
592
|
+
#
|
593
|
+
# @see Hanami::Controller::Configuration#format
|
594
|
+
#
|
595
|
+
# @since 0.2.0
|
596
|
+
# @api private
|
597
|
+
def format_for(mime_type)
|
598
|
+
@formats[mime_type]
|
599
|
+
end
|
600
|
+
|
601
|
+
# Returns a mime type for the given format
|
602
|
+
#
|
603
|
+
# @param format [#to_sym] a format
|
604
|
+
#
|
605
|
+
# @return [String,nil] the corresponding mime type, if present
|
606
|
+
#
|
607
|
+
# @since 0.2.0
|
608
|
+
# @api private
|
609
|
+
def mime_type_for(format)
|
610
|
+
@formats.key(format)
|
611
|
+
end
|
612
|
+
|
613
|
+
# Duplicate by copying the settings in a new instance.
|
614
|
+
#
|
615
|
+
# @return [Hanami::Controller::Configuration] a copy of the configuration
|
616
|
+
#
|
617
|
+
# @since 0.2.0
|
618
|
+
# @api private
|
619
|
+
def duplicate
|
620
|
+
Configuration.new.tap do |c|
|
621
|
+
c.handle_exceptions = handle_exceptions
|
622
|
+
c.handled_exceptions = handled_exceptions.dup
|
623
|
+
c.action_module = action_module
|
624
|
+
c.modules = modules.dup
|
625
|
+
c.formats = formats.dup
|
626
|
+
c.default_request_format = default_request_format
|
627
|
+
c.default_response_format = default_response_format
|
628
|
+
c.default_charset = default_charset
|
629
|
+
c.default_headers = default_headers.dup
|
630
|
+
c.cookies = cookies.dup
|
631
|
+
end
|
632
|
+
end
|
633
|
+
|
634
|
+
# Return included modules
|
635
|
+
#
|
636
|
+
# @return [Array<Proc>] array of included blocks
|
637
|
+
#
|
638
|
+
# @since 0.2.0
|
639
|
+
# @api private
|
640
|
+
#
|
641
|
+
# @see Hanami::Controller::Configuration#prepare
|
642
|
+
attr_reader :modules
|
643
|
+
|
644
|
+
# Reset all the values to the defaults
|
645
|
+
#
|
646
|
+
# @since 0.2.0
|
647
|
+
# @api private
|
648
|
+
def reset!
|
649
|
+
@handle_exceptions = true
|
650
|
+
@handled_exceptions = {}
|
651
|
+
@modules = []
|
652
|
+
@formats = DEFAULT_FORMATS.dup
|
653
|
+
@default_request_format = nil
|
654
|
+
@default_response_format = nil
|
655
|
+
@default_charset = nil
|
656
|
+
@default_headers = {}
|
657
|
+
@cookies = {}
|
658
|
+
@action_module = ::Hanami::Action
|
659
|
+
end
|
660
|
+
|
661
|
+
# Copy the configuration for the given action
|
662
|
+
#
|
663
|
+
# @param base [Class] the target action
|
664
|
+
#
|
665
|
+
# @return void
|
666
|
+
#
|
667
|
+
# @since 0.3.0
|
668
|
+
# @api private
|
669
|
+
#
|
670
|
+
# @see Hanami::Controller::Configurable.included
|
671
|
+
def copy!(base)
|
672
|
+
modules.each do |mod|
|
673
|
+
base.class_eval(&mod)
|
674
|
+
end
|
675
|
+
end
|
676
|
+
|
677
|
+
# Load the framework
|
678
|
+
#
|
679
|
+
# @since 0.3.0
|
680
|
+
# @api private
|
681
|
+
def load!
|
682
|
+
freeze
|
683
|
+
end
|
684
|
+
|
685
|
+
protected
|
686
|
+
# @since 0.5.0
|
687
|
+
# @api private
|
688
|
+
def _sort_handled_exceptions!
|
689
|
+
@handled_exceptions = Hash[
|
690
|
+
@handled_exceptions.sort{|(ex1,_),(ex2,_)| ex1.ancestors.include?(ex2) ? -1 : 1 }
|
691
|
+
]
|
692
|
+
end
|
693
|
+
|
694
|
+
attr_accessor :handled_exceptions
|
695
|
+
attr_accessor :formats
|
696
|
+
attr_writer :action_module
|
697
|
+
attr_writer :modules
|
698
|
+
attr_writer :default_request_format
|
699
|
+
attr_writer :default_response_format
|
700
|
+
attr_writer :default_charset
|
701
|
+
attr_writer :default_headers
|
702
|
+
attr_writer :cookies
|
703
|
+
end
|
704
|
+
end
|
705
|
+
end
|