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,47 @@
|
|
1
|
+
|
2
|
+
module Hanami
|
3
|
+
module Action
|
4
|
+
module Rack
|
5
|
+
module Callable
|
6
|
+
# Callable module for actions. With this module, actions with middlewares
|
7
|
+
# will be able to work with rack builder.
|
8
|
+
#
|
9
|
+
# @param env [Hash] the full Rack env or the params. This value may vary,
|
10
|
+
# see the examples below.
|
11
|
+
#
|
12
|
+
# @since 0.4.0
|
13
|
+
#
|
14
|
+
# @see Hanami::Action::Rack::ClassMethods#rack_builder
|
15
|
+
# @see Hanami::Action::Rack::ClassMethods#use
|
16
|
+
#
|
17
|
+
# @example
|
18
|
+
# require 'hanami/controller'
|
19
|
+
#
|
20
|
+
# class MyMiddleware
|
21
|
+
# def initialize(app)
|
22
|
+
# @app = app
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
# def call(env)
|
26
|
+
# #...
|
27
|
+
# end
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
# class Show
|
31
|
+
# include Hanami::Action
|
32
|
+
# use MyMiddleware
|
33
|
+
#
|
34
|
+
# def call(params)
|
35
|
+
# # ...
|
36
|
+
# puts params # => { id: 23 } extracted from Rack env
|
37
|
+
# end
|
38
|
+
# end
|
39
|
+
#
|
40
|
+
# Show.respond_to?(:call) # => true
|
41
|
+
def call(env)
|
42
|
+
rack_builder.call(env)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'rack/file'
|
2
|
+
|
3
|
+
module Hanami
|
4
|
+
module Action
|
5
|
+
module Rack
|
6
|
+
# File to be sent
|
7
|
+
#
|
8
|
+
# @since 0.4.3
|
9
|
+
# @api private
|
10
|
+
#
|
11
|
+
# @see Hanami::Action::Rack#send_file
|
12
|
+
class File
|
13
|
+
# @param path [String,Pathname] file path
|
14
|
+
#
|
15
|
+
# @since 0.4.3
|
16
|
+
# @api private
|
17
|
+
def initialize(path)
|
18
|
+
@file = ::Rack::File.new(nil)
|
19
|
+
@path = path
|
20
|
+
end
|
21
|
+
|
22
|
+
# @since 0.4.3
|
23
|
+
# @api private
|
24
|
+
def call(env)
|
25
|
+
@file.path = @path.to_s
|
26
|
+
@file.serving(env)
|
27
|
+
rescue Errno::ENOENT
|
28
|
+
[404, {}, nil]
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module Hanami
|
2
|
+
module Action
|
3
|
+
# HTTP redirect API
|
4
|
+
#
|
5
|
+
# @since 0.1.0
|
6
|
+
module Redirect
|
7
|
+
# The HTTP header for redirects
|
8
|
+
#
|
9
|
+
# @since 0.2.0
|
10
|
+
# @api private
|
11
|
+
LOCATION = 'Location'.freeze
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
# Redirect to the given URL and halt the request
|
16
|
+
#
|
17
|
+
# @param url [String] the destination URL
|
18
|
+
# @param status [Fixnum] the http code
|
19
|
+
#
|
20
|
+
# @since 0.1.0
|
21
|
+
#
|
22
|
+
# @see Hanami::Action::Throwable#halt
|
23
|
+
#
|
24
|
+
# @example With default status code (302)
|
25
|
+
# require 'hanami/controller'
|
26
|
+
#
|
27
|
+
# class Create
|
28
|
+
# include Hanami::Action
|
29
|
+
#
|
30
|
+
# def call(params)
|
31
|
+
# # ...
|
32
|
+
# redirect_to 'http://example.com/articles/23'
|
33
|
+
# end
|
34
|
+
# end
|
35
|
+
#
|
36
|
+
# action = Create.new
|
37
|
+
# action.call({}) # => [302, {'Location' => '/articles/23'}, '']
|
38
|
+
#
|
39
|
+
# @example With custom status code
|
40
|
+
# require 'hanami/controller'
|
41
|
+
#
|
42
|
+
# class Create
|
43
|
+
# include Hanami::Action
|
44
|
+
#
|
45
|
+
# def call(params)
|
46
|
+
# # ...
|
47
|
+
# redirect_to 'http://example.com/articles/23', status: 301
|
48
|
+
# end
|
49
|
+
# end
|
50
|
+
#
|
51
|
+
# action = Create.new
|
52
|
+
# action.call({}) # => [301, {'Location' => '/articles/23'}, '']
|
53
|
+
def redirect_to(url, status: 302)
|
54
|
+
headers[LOCATION] = ::String.new(url)
|
55
|
+
halt(status)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
require 'rack/request'
|
2
|
+
|
3
|
+
module Hanami
|
4
|
+
module Action
|
5
|
+
# An HTTP request based on top of Rack::Request.
|
6
|
+
# This guarantees backwards compatibility with with Rack.
|
7
|
+
#
|
8
|
+
# @since 0.3.1
|
9
|
+
#
|
10
|
+
# @see http://www.rubydoc.info/gems/rack/Rack/Request
|
11
|
+
class Request < ::Rack::Request
|
12
|
+
|
13
|
+
# @raise [NotImplementedError]
|
14
|
+
#
|
15
|
+
# @since 0.3.1
|
16
|
+
# @api private
|
17
|
+
def content_type
|
18
|
+
raise NotImplementedError, 'Please use Action#content_type'
|
19
|
+
end
|
20
|
+
|
21
|
+
# @raise [NotImplementedError]
|
22
|
+
#
|
23
|
+
# @since 0.3.1
|
24
|
+
# @api private
|
25
|
+
def session
|
26
|
+
raise NotImplementedError, 'Please include Action::Session and use Action#session'
|
27
|
+
end
|
28
|
+
|
29
|
+
# @raise [NotImplementedError]
|
30
|
+
#
|
31
|
+
# @since 0.3.1
|
32
|
+
# @api private
|
33
|
+
def cookies
|
34
|
+
raise NotImplementedError, 'Please include Action::Cookies and use Action#cookies'
|
35
|
+
end
|
36
|
+
|
37
|
+
# @raise [NotImplementedError]
|
38
|
+
#
|
39
|
+
# @since 0.3.1
|
40
|
+
# @api private
|
41
|
+
def params
|
42
|
+
raise NotImplementedError, 'Please use params passed to Action#call'
|
43
|
+
end
|
44
|
+
|
45
|
+
# @raise [NotImplementedError]
|
46
|
+
#
|
47
|
+
# @since 0.3.1
|
48
|
+
# @api private
|
49
|
+
def update_param(*)
|
50
|
+
raise NotImplementedError, 'Please use params passed to Action#call'
|
51
|
+
end
|
52
|
+
|
53
|
+
# @raise [NotImplementedError]
|
54
|
+
#
|
55
|
+
# @since 0.3.1
|
56
|
+
# @api private
|
57
|
+
def delete_param(*)
|
58
|
+
raise NotImplementedError, 'Please use params passed to Action#call'
|
59
|
+
end
|
60
|
+
|
61
|
+
# @raise [NotImplementedError]
|
62
|
+
#
|
63
|
+
# @since 0.3.1
|
64
|
+
# @api private
|
65
|
+
def [](*)
|
66
|
+
raise NotImplementedError, 'Please use params passed to Action#call'
|
67
|
+
end
|
68
|
+
|
69
|
+
# @raise [NotImplementedError]
|
70
|
+
#
|
71
|
+
# @since 0.3.1
|
72
|
+
# @api private
|
73
|
+
def []=(*)
|
74
|
+
raise NotImplementedError, 'Please use params passed to Action#call'
|
75
|
+
end
|
76
|
+
|
77
|
+
# @raise [NotImplementedError]
|
78
|
+
#
|
79
|
+
# @since 0.3.1
|
80
|
+
# @api private
|
81
|
+
def values_at(*)
|
82
|
+
raise NotImplementedError, 'Please use params passed to Action#call'
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,154 @@
|
|
1
|
+
require 'hanami/action/flash'
|
2
|
+
|
3
|
+
module Hanami
|
4
|
+
module Action
|
5
|
+
# Session API
|
6
|
+
#
|
7
|
+
# This module isn't included by default.
|
8
|
+
#
|
9
|
+
# @since 0.1.0
|
10
|
+
module Session
|
11
|
+
# The key that returns raw session from the Rack env
|
12
|
+
#
|
13
|
+
# @since 0.1.0
|
14
|
+
# @api private
|
15
|
+
SESSION_KEY = 'rack.session'.freeze
|
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
|
+
|
23
|
+
# Add session to default exposures
|
24
|
+
#
|
25
|
+
# @since 0.4.4
|
26
|
+
# @api private
|
27
|
+
def self.included(action)
|
28
|
+
action.class_eval do
|
29
|
+
expose :session
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Gets the session from the request and expose it as an Hash.
|
34
|
+
#
|
35
|
+
# @return [Hash] the HTTP session from the request
|
36
|
+
#
|
37
|
+
# @since 0.1.0
|
38
|
+
#
|
39
|
+
# @example
|
40
|
+
# require 'hanami/controller'
|
41
|
+
# require 'hanami/action/session'
|
42
|
+
#
|
43
|
+
# class Show
|
44
|
+
# include Hanami::Action
|
45
|
+
# include Hanami::Action::Session
|
46
|
+
#
|
47
|
+
# def call(params)
|
48
|
+
# # ...
|
49
|
+
#
|
50
|
+
# # get a value
|
51
|
+
# session[:user_id] # => '23'
|
52
|
+
#
|
53
|
+
# # set a value
|
54
|
+
# session[:foo] = 'bar'
|
55
|
+
#
|
56
|
+
# # remove a value
|
57
|
+
# session[:bax] = nil
|
58
|
+
# end
|
59
|
+
# end
|
60
|
+
def session
|
61
|
+
@_env[SESSION_KEY] ||= {}
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
# Container useful to transport data with the HTTP session
|
67
|
+
#
|
68
|
+
# @return [Hanami::Action::Flash] a Flash instance
|
69
|
+
#
|
70
|
+
# @since 0.3.0
|
71
|
+
# @api private
|
72
|
+
#
|
73
|
+
# @see Hanami::Action::Flash
|
74
|
+
def flash
|
75
|
+
@flash ||= Flash.new(session, request_id)
|
76
|
+
end
|
77
|
+
|
78
|
+
# In case of validations errors, preserve those informations after a
|
79
|
+
# redirect.
|
80
|
+
#
|
81
|
+
# @return [void]
|
82
|
+
#
|
83
|
+
# @since 0.3.0
|
84
|
+
# @api private
|
85
|
+
#
|
86
|
+
# @see Hanami::Action::Redirect#redirect_to
|
87
|
+
#
|
88
|
+
# @example
|
89
|
+
# require 'hanami/controller'
|
90
|
+
#
|
91
|
+
# module Comments
|
92
|
+
# class Index
|
93
|
+
# include Hanami::Action
|
94
|
+
# include Hanami::Action::Session
|
95
|
+
#
|
96
|
+
# expose :comments
|
97
|
+
#
|
98
|
+
# def call(params)
|
99
|
+
# @comments = CommentRepository.all
|
100
|
+
# end
|
101
|
+
# end
|
102
|
+
#
|
103
|
+
# class Create
|
104
|
+
# include Hanami::Action
|
105
|
+
# include Hanami::Action::Session
|
106
|
+
#
|
107
|
+
# params do
|
108
|
+
# param :text, type: String, presence: true
|
109
|
+
# end
|
110
|
+
#
|
111
|
+
# def call(params)
|
112
|
+
# comment = Comment.new(params)
|
113
|
+
# CommentRepository.create(comment) if params.valid?
|
114
|
+
#
|
115
|
+
# redirect_to '/comments'
|
116
|
+
# end
|
117
|
+
# end
|
118
|
+
# end
|
119
|
+
#
|
120
|
+
# # The validation errors caused by Comments::Create are available
|
121
|
+
# # **after the redirect** in the context of Comments::Index.
|
122
|
+
def redirect_to(*args)
|
123
|
+
flash[ERRORS_KEY] = errors.to_a unless params.valid?
|
124
|
+
super
|
125
|
+
end
|
126
|
+
|
127
|
+
# Read errors from flash or delegate to the superclass
|
128
|
+
#
|
129
|
+
# @return [Hanami::Validations::Errors] A collection of validation errors
|
130
|
+
#
|
131
|
+
# @since 0.3.0
|
132
|
+
# @api private
|
133
|
+
#
|
134
|
+
# @see Hanami::Action::Validatable
|
135
|
+
# @see Hanami::Action::Session#flash
|
136
|
+
def errors
|
137
|
+
flash[ERRORS_KEY] || super
|
138
|
+
end
|
139
|
+
|
140
|
+
# Finalize the response
|
141
|
+
#
|
142
|
+
# @return [void]
|
143
|
+
#
|
144
|
+
# @since 0.3.0
|
145
|
+
# @api private
|
146
|
+
#
|
147
|
+
# @see Hanami::Action#finish
|
148
|
+
def finish
|
149
|
+
super
|
150
|
+
flash.clear
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
@@ -0,0 +1,194 @@
|
|
1
|
+
require 'hanami/utils/class_attribute'
|
2
|
+
require 'hanami/http/status'
|
3
|
+
|
4
|
+
module Hanami
|
5
|
+
module Action
|
6
|
+
# Throw API
|
7
|
+
#
|
8
|
+
# @since 0.1.0
|
9
|
+
#
|
10
|
+
# @see Hanami::Action::Throwable::ClassMethods#handle_exception
|
11
|
+
# @see Hanami::Action::Throwable#halt
|
12
|
+
# @see Hanami::Action::Throwable#status
|
13
|
+
module Throwable
|
14
|
+
# @since 0.2.0
|
15
|
+
# @api private
|
16
|
+
RACK_ERRORS = 'rack.errors'.freeze
|
17
|
+
|
18
|
+
# This isn't part of Rack SPEC
|
19
|
+
#
|
20
|
+
# Exception notifiers use <tt>rack.exception</tt> instead of
|
21
|
+
# <tt>rack.errors</tt>, so we need to support it.
|
22
|
+
#
|
23
|
+
# @since 0.5.0
|
24
|
+
# @api private
|
25
|
+
#
|
26
|
+
# @see Hanami::Action::Throwable::RACK_ERRORS
|
27
|
+
# @see http://www.rubydoc.info/github/rack/rack/file/SPEC#The_Error_Stream
|
28
|
+
# @see https://github.com/hanami/controller/issues/133
|
29
|
+
RACK_EXCEPTION = 'rack.exception'.freeze
|
30
|
+
|
31
|
+
def self.included(base)
|
32
|
+
base.extend ClassMethods
|
33
|
+
end
|
34
|
+
|
35
|
+
# Throw API class methods
|
36
|
+
#
|
37
|
+
# @since 0.1.0
|
38
|
+
# @api private
|
39
|
+
module ClassMethods
|
40
|
+
private
|
41
|
+
|
42
|
+
# Handle the given exception with an HTTP status code.
|
43
|
+
#
|
44
|
+
# When the exception is raise during #call execution, it will be
|
45
|
+
# translated into the associated HTTP status.
|
46
|
+
#
|
47
|
+
# This is a fine grained control, for a global configuration see
|
48
|
+
# Hanami::Action.handled_exceptions
|
49
|
+
#
|
50
|
+
# @param exception [Hash] the exception class must be the key and the
|
51
|
+
# HTTP status the value of the hash
|
52
|
+
#
|
53
|
+
# @since 0.1.0
|
54
|
+
#
|
55
|
+
# @see Hanami::Action.handled_exceptions
|
56
|
+
#
|
57
|
+
# @example
|
58
|
+
# require 'hanami/controller'
|
59
|
+
#
|
60
|
+
# class Show
|
61
|
+
# include Hanami::Action
|
62
|
+
# handle_exception RecordNotFound => 404
|
63
|
+
#
|
64
|
+
# def call(params)
|
65
|
+
# # ...
|
66
|
+
# raise RecordNotFound.new
|
67
|
+
# end
|
68
|
+
# end
|
69
|
+
#
|
70
|
+
# Show.new.call({id: 1}) # => [404, {}, ['Not Found']]
|
71
|
+
def handle_exception(exception)
|
72
|
+
configuration.handle_exception(exception)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
protected
|
77
|
+
|
78
|
+
# Halt the action execution with the given HTTP status code and message.
|
79
|
+
#
|
80
|
+
# When used, the execution of a callback or of an action is interrupted
|
81
|
+
# and the control returns to the framework, that decides how to handle
|
82
|
+
# the event.
|
83
|
+
#
|
84
|
+
# If a message is provided, it sets the response body with the message.
|
85
|
+
# Otherwise, it sets the response body with the default message associated
|
86
|
+
# to the code (eg 404 will set `"Not Found"`).
|
87
|
+
#
|
88
|
+
# @param code [Fixnum] a valid HTTP status code
|
89
|
+
# @param message [String] the response body
|
90
|
+
#
|
91
|
+
# @since 0.2.0
|
92
|
+
#
|
93
|
+
# @see Hanami::Controller#handled_exceptions
|
94
|
+
# @see Hanami::Action::Throwable#handle_exception
|
95
|
+
# @see Hanami::Http::Status:ALL
|
96
|
+
#
|
97
|
+
# @example Basic usage
|
98
|
+
# require 'hanami/controller'
|
99
|
+
#
|
100
|
+
# class Show
|
101
|
+
# def call(params)
|
102
|
+
# halt 404
|
103
|
+
# end
|
104
|
+
# end
|
105
|
+
#
|
106
|
+
# # => [404, {}, ["Not Found"]]
|
107
|
+
#
|
108
|
+
# @example Custom message
|
109
|
+
# require 'hanami/controller'
|
110
|
+
#
|
111
|
+
# class Show
|
112
|
+
# def call(params)
|
113
|
+
# halt 404, "This is not the droid you're looking for."
|
114
|
+
# end
|
115
|
+
# end
|
116
|
+
#
|
117
|
+
# # => [404, {}, ["This is not the droid you're looking for."]]
|
118
|
+
def halt(code, message = nil)
|
119
|
+
message ||= Http::Status.message_for(code)
|
120
|
+
status(code, message)
|
121
|
+
|
122
|
+
throw :halt
|
123
|
+
end
|
124
|
+
|
125
|
+
# Sets the given code and message for the response
|
126
|
+
#
|
127
|
+
# @param code [Fixnum] a valid HTTP status code
|
128
|
+
# @param message [String] the response body
|
129
|
+
#
|
130
|
+
# @since 0.1.0
|
131
|
+
# @see Hanami::Http::Status:ALL
|
132
|
+
def status(code, message)
|
133
|
+
self.status = code
|
134
|
+
self.body = message
|
135
|
+
end
|
136
|
+
|
137
|
+
private
|
138
|
+
# @since 0.1.0
|
139
|
+
# @api private
|
140
|
+
def _rescue
|
141
|
+
catch :halt do
|
142
|
+
begin
|
143
|
+
yield
|
144
|
+
rescue => exception
|
145
|
+
_reference_in_rack_errors(exception)
|
146
|
+
_handle_exception(exception)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
# @since 0.2.0
|
152
|
+
# @api private
|
153
|
+
def _reference_in_rack_errors(exception)
|
154
|
+
return if configuration.handled_exception?(exception)
|
155
|
+
|
156
|
+
@_env[RACK_EXCEPTION] = exception
|
157
|
+
|
158
|
+
if errors = @_env[RACK_ERRORS]
|
159
|
+
errors.write(_dump_exception(exception))
|
160
|
+
errors.flush
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
# @since 0.2.0
|
165
|
+
# @api private
|
166
|
+
def _dump_exception(exception)
|
167
|
+
[[exception.class, exception.message].compact.join(": "), *exception.backtrace].join("\n\t")
|
168
|
+
end
|
169
|
+
|
170
|
+
# @since 0.1.0
|
171
|
+
# @api private
|
172
|
+
def _handle_exception(exception)
|
173
|
+
raise unless configuration.handle_exceptions
|
174
|
+
|
175
|
+
instance_exec(
|
176
|
+
exception,
|
177
|
+
&_exception_handler(exception)
|
178
|
+
)
|
179
|
+
end
|
180
|
+
|
181
|
+
# @since 0.3.0
|
182
|
+
# @api private
|
183
|
+
def _exception_handler(exception)
|
184
|
+
handler = configuration.exception_handler(exception)
|
185
|
+
|
186
|
+
if respond_to?(handler.to_s, true)
|
187
|
+
method(handler)
|
188
|
+
else
|
189
|
+
->(ex) { halt handler }
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|