hanami 2.0.3 → 2.1.0.beta2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +37 -2
- data/LICENSE.md +1 -1
- data/README.md +26 -10
- data/hanami.gemspec +2 -2
- data/lib/hanami/app.rb +5 -0
- data/lib/hanami/config/actions.rb +4 -11
- data/lib/hanami/config/assets.rb +84 -0
- data/lib/hanami/config/null_config.rb +3 -0
- data/lib/hanami/config/views.rb +0 -4
- data/lib/hanami/config.rb +71 -5
- data/lib/hanami/extensions/action/slice_configured_action.rb +15 -7
- data/lib/hanami/extensions/action.rb +8 -6
- data/lib/hanami/extensions/router/errors.rb +58 -0
- data/lib/hanami/extensions/view/context.rb +129 -60
- data/lib/hanami/extensions/view/part.rb +26 -0
- data/lib/hanami/extensions/view/scope.rb +26 -0
- data/lib/hanami/extensions/view/slice_configured_context.rb +0 -2
- data/lib/hanami/extensions/view/slice_configured_helpers.rb +44 -0
- data/lib/hanami/extensions/view/slice_configured_view.rb +106 -21
- data/lib/hanami/extensions/view/standard_helpers.rb +18 -0
- data/lib/hanami/extensions.rb +10 -3
- data/lib/hanami/helpers/assets_helper.rb +752 -0
- data/lib/hanami/helpers/form_helper/form_builder.rb +1391 -0
- data/lib/hanami/helpers/form_helper/values.rb +75 -0
- data/lib/hanami/helpers/form_helper.rb +213 -0
- data/lib/hanami/middleware/assets.rb +21 -0
- data/lib/hanami/middleware/public_errors_app.rb +75 -0
- data/lib/hanami/middleware/render_errors.rb +90 -0
- data/lib/hanami/providers/assets.rb +44 -0
- data/lib/hanami/rake_tasks.rb +19 -18
- data/lib/hanami/settings.rb +1 -1
- data/lib/hanami/slice.rb +48 -2
- data/lib/hanami/slice_configurable.rb +3 -2
- data/lib/hanami/version.rb +1 -1
- data/lib/hanami/web/rack_logger.rb +1 -1
- data/lib/hanami.rb +3 -3
- data/spec/integration/action/view_rendering/view_context_spec.rb +221 -0
- data/spec/integration/action/view_rendering_spec.rb +0 -18
- data/spec/integration/assets/assets_spec.rb +101 -0
- data/spec/integration/assets/serve_static_assets_spec.rb +152 -0
- data/spec/integration/logging/exception_logging_spec.rb +115 -0
- data/spec/integration/logging/notifications_spec.rb +68 -0
- data/spec/integration/logging/request_logging_spec.rb +128 -0
- data/spec/integration/rack_app/middleware_spec.rb +22 -22
- data/spec/integration/rack_app/rack_app_spec.rb +3 -220
- data/spec/integration/rake_tasks_spec.rb +107 -0
- data/spec/integration/view/config/default_context_spec.rb +149 -0
- data/spec/integration/view/{inflector_spec.rb → config/inflector_spec.rb} +1 -1
- data/spec/integration/view/config/part_class_spec.rb +147 -0
- data/spec/integration/view/config/part_namespace_spec.rb +103 -0
- data/spec/integration/view/config/paths_spec.rb +119 -0
- data/spec/integration/view/config/scope_class_spec.rb +147 -0
- data/spec/integration/view/config/scope_namespace_spec.rb +103 -0
- data/spec/integration/view/config/template_spec.rb +38 -0
- data/spec/integration/view/context/assets_spec.rb +3 -9
- data/spec/integration/view/context/request_spec.rb +3 -7
- data/spec/integration/view/helpers/form_helper_spec.rb +174 -0
- data/spec/integration/view/helpers/part_helpers_spec.rb +124 -0
- data/spec/integration/view/helpers/scope_helpers_spec.rb +84 -0
- data/spec/integration/view/helpers/user_defined_helpers/part_helpers_spec.rb +162 -0
- data/spec/integration/view/helpers/user_defined_helpers/scope_helpers_spec.rb +119 -0
- data/spec/integration/view/slice_configuration_spec.rb +9 -9
- data/spec/integration/web/render_detailed_errors_spec.rb +107 -0
- data/spec/integration/web/render_errors_spec.rb +242 -0
- data/spec/spec_helper.rb +1 -1
- data/spec/support/app_integration.rb +46 -2
- data/spec/support/matchers.rb +32 -0
- data/spec/unit/hanami/config/actions/content_security_policy_spec.rb +24 -36
- data/spec/unit/hanami/config/actions/csrf_protection_spec.rb +4 -3
- data/spec/unit/hanami/config/actions/default_values_spec.rb +3 -6
- data/spec/unit/hanami/config/render_detailed_errors_spec.rb +25 -0
- data/spec/unit/hanami/config/render_errors_spec.rb +25 -0
- data/spec/unit/hanami/config/views_spec.rb +0 -18
- data/spec/unit/hanami/env_spec.rb +11 -25
- data/spec/unit/hanami/extensions/view/context_spec.rb +59 -0
- data/spec/unit/hanami/helpers/assets_helper/asset_url_spec.rb +109 -0
- data/spec/unit/hanami/helpers/assets_helper/audio_tag_spec.rb +132 -0
- data/spec/unit/hanami/helpers/assets_helper/favicon_link_tag_spec.rb +91 -0
- data/spec/unit/hanami/helpers/assets_helper/image_tag_spec.rb +92 -0
- data/spec/unit/hanami/helpers/assets_helper/javascript_tag_spec.rb +143 -0
- data/spec/unit/hanami/helpers/assets_helper/stylesheet_link_tag_spec.rb +126 -0
- data/spec/unit/hanami/helpers/assets_helper/video_tag_spec.rb +132 -0
- data/spec/unit/hanami/helpers/form_helper_spec.rb +2826 -0
- data/spec/unit/hanami/router/errors/not_allowed_error_spec.rb +27 -0
- data/spec/unit/hanami/router/errors/not_found_error_spec.rb +22 -0
- data/spec/unit/hanami/slice_configurable_spec.rb +18 -0
- data/spec/unit/hanami/version_spec.rb +1 -1
- data/spec/unit/hanami/web/rack_logger_spec.rb +1 -1
- metadata +95 -35
- data/lib/hanami/assets/app_config.rb +0 -61
- data/lib/hanami/assets/config.rb +0 -53
- data/spec/integration/action/view_integration_spec.rb +0 -165
- data/spec/integration/view/part_namespace_spec.rb +0 -96
- data/spec/integration/view/path_spec.rb +0 -56
- data/spec/integration/view/template_spec.rb +0 -68
- data/spec/isolation/hanami/application/already_configured_spec.rb +0 -19
- data/spec/isolation/hanami/application/inherit_anonymous_class_spec.rb +0 -10
- data/spec/isolation/hanami/application/inherit_concrete_class_spec.rb +0 -14
- data/spec/isolation/hanami/application/not_configured_spec.rb +0 -9
- data/spec/isolation/hanami/application/routes/configured_spec.rb +0 -44
- data/spec/isolation/hanami/application/routes/not_configured_spec.rb +0 -16
- data/spec/isolation/hanami/boot/success_spec.rb +0 -50
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Hanami
|
4
|
+
module Helpers
|
5
|
+
module FormHelper
|
6
|
+
# Values from params and form helpers.
|
7
|
+
#
|
8
|
+
# It's responsible to populate input values with data coming from params
|
9
|
+
# and inline values specified via form helpers like `text_field`.
|
10
|
+
#
|
11
|
+
# @since 2.0.0
|
12
|
+
# @api private
|
13
|
+
class Values
|
14
|
+
# @since 2.0.0
|
15
|
+
# @api private
|
16
|
+
GET_SEPARATOR = "."
|
17
|
+
|
18
|
+
# @api private
|
19
|
+
# @since 2.0.0
|
20
|
+
attr_reader :csrf_token
|
21
|
+
|
22
|
+
# @since 2.0.0
|
23
|
+
# @api private
|
24
|
+
def initialize(values: {}, params: {}, csrf_token: nil)
|
25
|
+
@values = values.to_h
|
26
|
+
@params = params.to_h
|
27
|
+
@csrf_token = csrf_token
|
28
|
+
end
|
29
|
+
|
30
|
+
# Returns the value (if present) for the given key.
|
31
|
+
# Nested values are expressed with an array if symbols.
|
32
|
+
#
|
33
|
+
# @since 2.0.0
|
34
|
+
# @api private
|
35
|
+
def get(*keys)
|
36
|
+
get_from_params(*keys) || get_from_values(*keys)
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
# @since 2.0.0
|
42
|
+
# @api private
|
43
|
+
def get_from_params(*keys)
|
44
|
+
keys.map! { |key| /\A\d+\z/.match?(key.to_s) ? key.to_s.to_i : key }
|
45
|
+
@params.dig(*keys)
|
46
|
+
end
|
47
|
+
|
48
|
+
# @since 2.0.0
|
49
|
+
# @api private
|
50
|
+
def get_from_values(*keys)
|
51
|
+
head, *tail = *keys
|
52
|
+
result = @values[head]
|
53
|
+
|
54
|
+
tail.each do |k|
|
55
|
+
break if result.nil?
|
56
|
+
|
57
|
+
result = dig(result, k)
|
58
|
+
end
|
59
|
+
|
60
|
+
result
|
61
|
+
end
|
62
|
+
|
63
|
+
# @since 2.0.0
|
64
|
+
# @api private
|
65
|
+
def dig(base, key)
|
66
|
+
case base
|
67
|
+
when ::Hash then base[key]
|
68
|
+
when Array then base[key.to_s.to_i]
|
69
|
+
when ->(r) { r.respond_to?(key) } then base.public_send(key)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,213 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "hanami/view"
|
4
|
+
|
5
|
+
module Hanami
|
6
|
+
module Helpers
|
7
|
+
# Helper methods for generating HTML forms.
|
8
|
+
#
|
9
|
+
# These helpers will be automatically available in your view templates, part classes and scope
|
10
|
+
# classes.
|
11
|
+
#
|
12
|
+
# This module provides one primary method: {#form_for}, yielding an HTML form builder. This
|
13
|
+
# integrates with request params and template locals to populate the form with appropriate
|
14
|
+
# values.
|
15
|
+
#
|
16
|
+
# @api public
|
17
|
+
# @since 2.0.0
|
18
|
+
module FormHelper
|
19
|
+
require_relative "form_helper/form_builder"
|
20
|
+
|
21
|
+
# Default HTTP method for form
|
22
|
+
#
|
23
|
+
# @since 2.0.0
|
24
|
+
# @api private
|
25
|
+
DEFAULT_METHOD = "POST"
|
26
|
+
|
27
|
+
# Default charset
|
28
|
+
#
|
29
|
+
# @since 2.0.0
|
30
|
+
# @api private
|
31
|
+
DEFAULT_CHARSET = "utf-8"
|
32
|
+
|
33
|
+
# CSRF Token session key
|
34
|
+
#
|
35
|
+
# This name of this key is shared with the hanami and hanami-controller gems.
|
36
|
+
#
|
37
|
+
# @since 2.0.0
|
38
|
+
# @api private
|
39
|
+
CSRF_TOKEN = :_csrf_token
|
40
|
+
|
41
|
+
include Hanami::View::Helpers::TagHelper
|
42
|
+
|
43
|
+
# Yields a form builder for constructing an HTML form and returns the resulting form string.
|
44
|
+
#
|
45
|
+
# See {FormHelper::FormBuilder} for the methods for building the form's fields.
|
46
|
+
#
|
47
|
+
# @overload form_for(base_name, url, values: _form_for_values, params: _form_for_params, **attributes)
|
48
|
+
# Builds the form using the given base name for all fields.
|
49
|
+
#
|
50
|
+
# @param base_name [String] the base
|
51
|
+
# @param url [String] the URL for submitting the form
|
52
|
+
# @param values [Hash] values to be used for populating form field values; optional,
|
53
|
+
# defaults to the template's locals or to a part's `{name => self}`
|
54
|
+
# @param params [Hash] request param values to be used for populating form field values;
|
55
|
+
# these are used in preference over the `values`; optional, defaults to the current
|
56
|
+
# request's params
|
57
|
+
# @param attributes [Hash] the HTML attributes for the form tag
|
58
|
+
# @yieldparam [FormHelper::FormBuilder] f the form builder
|
59
|
+
#
|
60
|
+
# @overload form_for(url, values: _form_for_values, params: _form_for_params, **attributes)
|
61
|
+
# @param url [String] the URL for submitting the form
|
62
|
+
# @param values [Hash] values to be used for populating form field values; optional,
|
63
|
+
# defaults to the template's locals or to a part's `{name => self}`
|
64
|
+
# @param params [Hash] request param values to be used for populating form field values;
|
65
|
+
# these are used in preference over the `values`; optional, defaults to the current
|
66
|
+
# request's params
|
67
|
+
# @param attributes [Hash] the HTML attributes for the form tag
|
68
|
+
# @yieldparam [FormHelper::FormBuilder] f the form builder
|
69
|
+
#
|
70
|
+
# @return [String] the form HTML
|
71
|
+
#
|
72
|
+
# @see FormHelper
|
73
|
+
# @see FormHelper::FormBuilder
|
74
|
+
#
|
75
|
+
# @example Basic usage
|
76
|
+
# <%= form_for("book", "/books", class: "form-horizontal") do |f| %>
|
77
|
+
# <div>
|
78
|
+
# <%= f.label "title" %>
|
79
|
+
# <%= f.text_field "title", class: "form-control" %>
|
80
|
+
# </div>
|
81
|
+
#
|
82
|
+
# <%= f.submit "Create" %>
|
83
|
+
# <% end %>
|
84
|
+
#
|
85
|
+
# =>
|
86
|
+
# <form action="/books" method="POST" accept-charset="utf-8" class="form-horizontal">
|
87
|
+
# <input type="hidden" name="_csrf_token" value="920cd5bfaecc6e58368950e790f2f7b4e5561eeeab230aa1b7de1b1f40ea7d5d">
|
88
|
+
# <div>
|
89
|
+
# <label for="book-title">Title</label>
|
90
|
+
# <input type="text" name="book[title]" id="book-title" value="Test Driven Development">
|
91
|
+
# </div>
|
92
|
+
#
|
93
|
+
# <button type="submit">Create</button>
|
94
|
+
# </form>
|
95
|
+
#
|
96
|
+
# @example Without base name
|
97
|
+
#
|
98
|
+
# <%= form_for("/books", class: "form-horizontal") do |f| %>
|
99
|
+
# <div>
|
100
|
+
# <%= f.label "books.title" %>
|
101
|
+
# <%= f.text_field "books.title", class: "form-control" %>
|
102
|
+
# </div>
|
103
|
+
#
|
104
|
+
# <%= f.submit "Create" %>
|
105
|
+
# <% end %>
|
106
|
+
#
|
107
|
+
# =>
|
108
|
+
# <form action="/books" method="POST" accept-charset="utf-8" class="form-horizontal">
|
109
|
+
# <input type="hidden" name="_csrf_token" value="920cd5bfaecc6e58368950e790f2f7b4e5561eeeab230aa1b7de1b1f40ea7d5d">
|
110
|
+
# <div>
|
111
|
+
# <label for="book-title">Title</label>
|
112
|
+
# <input type="text" name="book[title]" id="book-title" value="Test Driven Development">
|
113
|
+
# </div>
|
114
|
+
#
|
115
|
+
# <button type="submit">Create</button>
|
116
|
+
# </form>
|
117
|
+
#
|
118
|
+
# @example Method override
|
119
|
+
# <%= form_for("/books/123", method: :put) do |f|
|
120
|
+
# <%= f.text_field "book.title" %>
|
121
|
+
# <%= f.submit "Update" %>
|
122
|
+
# <% end %>
|
123
|
+
#
|
124
|
+
# =>
|
125
|
+
# <form action="/books/123" accept-charset="utf-8" method="POST">
|
126
|
+
# <input type="hidden" name="_method" value="PUT">
|
127
|
+
# <input type="hidden" name="_csrf_token" value="920cd5bfaecc6e58368950e790f2f7b4e5561eeeab230aa1b7de1b1f40ea7d5d">
|
128
|
+
# <input type="text" name="book[title]" id="book-title" value="Test Driven Development">
|
129
|
+
#
|
130
|
+
# <button type="submit">Update</button>
|
131
|
+
# </form>
|
132
|
+
#
|
133
|
+
# @example Overriding values
|
134
|
+
# <%= form_for("/songs", values: {song: {title: "Envision"}}) do |f| %>
|
135
|
+
# <%= f.text_field "song.title" %>
|
136
|
+
# <%= f.submit "Create" %>
|
137
|
+
# <%= end %>
|
138
|
+
#
|
139
|
+
# =>
|
140
|
+
# <form action="/songs" accept-charset="utf-8" method="POST">
|
141
|
+
# <input type="hidden" name="_csrf_token" value="920cd5bfaecc6e58368950e790f2f7b4e5561eeeab230aa1b7de1b1f40ea7d5d">
|
142
|
+
# <input type="text" name="song[title]" id="song-title" value="Envision">
|
143
|
+
#
|
144
|
+
# <button type="submit">Create</button>
|
145
|
+
# </form>
|
146
|
+
#
|
147
|
+
# @api public
|
148
|
+
# @since 2.0.0
|
149
|
+
def form_for(base_name, url = nil, values: _form_for_values, params: _form_for_params, **attributes)
|
150
|
+
url, base_name = base_name, nil if url.nil?
|
151
|
+
|
152
|
+
values = Values.new(values: values, params: params, csrf_token: _form_csrf_token)
|
153
|
+
|
154
|
+
builder = FormBuilder.new(
|
155
|
+
base_name: base_name,
|
156
|
+
values: values,
|
157
|
+
inflector: _context.inflector,
|
158
|
+
form_attributes: attributes
|
159
|
+
)
|
160
|
+
|
161
|
+
content = (block_given? ? yield(builder) : "").html_safe
|
162
|
+
|
163
|
+
builder.call(content, action: url, **attributes)
|
164
|
+
end
|
165
|
+
|
166
|
+
# Returns CSRF meta tags for use via unobtrusive JavaScript (UJS) libraries.
|
167
|
+
#
|
168
|
+
# @return [String, nil] the tags, if a CSRF token is available, or nil
|
169
|
+
#
|
170
|
+
# @example
|
171
|
+
# csrf_meta_tags
|
172
|
+
#
|
173
|
+
# =>
|
174
|
+
# <meta name="csrf-param" content="_csrf_token">
|
175
|
+
# <meta name="csrf-token" content="4a038be85b7603c406dcbfad4b9cdf91ec6ca138ed6441163a07bb0fdfbe25b5">
|
176
|
+
#
|
177
|
+
# @api public
|
178
|
+
# @since 2.0.0
|
179
|
+
def csrf_meta_tags
|
180
|
+
return unless (token = _form_csrf_token)
|
181
|
+
|
182
|
+
tag.meta(name: "csrf-param", content: CSRF_TOKEN) +
|
183
|
+
tag.meta(name: "csrf-token", content: token)
|
184
|
+
end
|
185
|
+
|
186
|
+
# @api private
|
187
|
+
# @since 2.0.0
|
188
|
+
def _form_for_values
|
189
|
+
if respond_to?(:_locals) # Scope
|
190
|
+
_locals
|
191
|
+
elsif respond_to?(:_name) # Part
|
192
|
+
{_name => self}
|
193
|
+
else
|
194
|
+
{}
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
# @api private
|
199
|
+
# @since 2.0.0
|
200
|
+
def _form_for_params
|
201
|
+
_context.request.params
|
202
|
+
end
|
203
|
+
|
204
|
+
# @since 2.0.0
|
205
|
+
# @api private
|
206
|
+
def _form_csrf_token
|
207
|
+
return unless _context.request.session_enabled?
|
208
|
+
|
209
|
+
_context.csrf_token
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rack/static"
|
4
|
+
|
5
|
+
module Hanami
|
6
|
+
module Middleware
|
7
|
+
class Assets < Rack::Static
|
8
|
+
def initialize(app, options = {}, config: Hanami.app.config)
|
9
|
+
root = config.actions.public_directory
|
10
|
+
urls = [config.assets.path_prefix]
|
11
|
+
|
12
|
+
defaults = {
|
13
|
+
root: root,
|
14
|
+
urls: urls
|
15
|
+
}
|
16
|
+
|
17
|
+
super(app, defaults.merge(options))
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rack"
|
4
|
+
|
5
|
+
module Hanami
|
6
|
+
module Middleware
|
7
|
+
# The errors app given to {Hanami::Middleware::RenderErrors}, which renders a error responses
|
8
|
+
# from HTML pages kept in `public/` or as simple JSON structures.
|
9
|
+
#
|
10
|
+
# @see Hanami::Middleware::RenderErrors
|
11
|
+
#
|
12
|
+
# @api private
|
13
|
+
# @since 2.1.0
|
14
|
+
class PublicErrorsApp
|
15
|
+
# @api private
|
16
|
+
# @since 2.1.0
|
17
|
+
attr_reader :public_path
|
18
|
+
|
19
|
+
# @api private
|
20
|
+
# @since 2.1.0
|
21
|
+
def initialize(public_path)
|
22
|
+
@public_path = public_path
|
23
|
+
end
|
24
|
+
|
25
|
+
# @api private
|
26
|
+
# @since 2.1.0
|
27
|
+
def call(env)
|
28
|
+
request = Rack::Request.new(env)
|
29
|
+
status = request.path_info[1..].to_i
|
30
|
+
content_type = request.get_header("HTTP_ACCEPT")
|
31
|
+
|
32
|
+
default_body = {
|
33
|
+
status: status,
|
34
|
+
error: Rack::Utils::HTTP_STATUS_CODES.fetch(status, Rack::Utils::HTTP_STATUS_CODES[500])
|
35
|
+
}
|
36
|
+
|
37
|
+
render(status, content_type, default_body)
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def render(status, content_type, default_body)
|
43
|
+
body, rendered_content_type = render_content(status, content_type, default_body)
|
44
|
+
|
45
|
+
[
|
46
|
+
status,
|
47
|
+
{
|
48
|
+
"Content-Type" => "#{rendered_content_type}; charset=utf-8",
|
49
|
+
"Content-Length" => body.bytesize.to_s
|
50
|
+
},
|
51
|
+
[body]
|
52
|
+
]
|
53
|
+
end
|
54
|
+
|
55
|
+
def render_content(status, content_type, default_body)
|
56
|
+
if content_type.to_s.start_with?("application/json")
|
57
|
+
require "json"
|
58
|
+
[JSON.generate(default_body), "application/json"]
|
59
|
+
else
|
60
|
+
[render_html_content(status, default_body), "text/html"]
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def render_html_content(status, default_body)
|
65
|
+
path = "#{public_path}/#{status}.html"
|
66
|
+
|
67
|
+
if File.exist?(path)
|
68
|
+
File.read(path)
|
69
|
+
else
|
70
|
+
default_body[:error]
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rack"
|
4
|
+
|
5
|
+
# rubocop:disable Lint/RescueException
|
6
|
+
|
7
|
+
module Hanami
|
8
|
+
module Middleware
|
9
|
+
# Rack middleware that rescues errors raised by the app renders friendly error responses, via a
|
10
|
+
# given "errors app".
|
11
|
+
#
|
12
|
+
# By default, this is enabled only in production mode.
|
13
|
+
#
|
14
|
+
# @see Hanami::Config#render_errors
|
15
|
+
# @see Hanani::Middleware::PublicErrorsApp
|
16
|
+
#
|
17
|
+
# @api private
|
18
|
+
# @since 2.1.0
|
19
|
+
class RenderErrors
|
20
|
+
# @api private
|
21
|
+
# @since 2.1.0
|
22
|
+
class RenderableException
|
23
|
+
attr_reader :exception
|
24
|
+
attr_reader :responses
|
25
|
+
|
26
|
+
# @api private
|
27
|
+
# @since 2.1.0
|
28
|
+
def initialize(exception, responses:)
|
29
|
+
@exception = exception
|
30
|
+
@responses = responses
|
31
|
+
end
|
32
|
+
|
33
|
+
# @api private
|
34
|
+
# @since 2.1.0
|
35
|
+
def rescue_response?
|
36
|
+
responses.key?(exception.class.name)
|
37
|
+
end
|
38
|
+
|
39
|
+
# @api private
|
40
|
+
# @since 2.1.0
|
41
|
+
def status_code
|
42
|
+
Rack::Utils.status_code(responses[exception.class.name])
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# @api private
|
47
|
+
# @since 2.1.0
|
48
|
+
def initialize(app, config, errors_app)
|
49
|
+
@app = app
|
50
|
+
@config = config
|
51
|
+
@errors_app = errors_app
|
52
|
+
end
|
53
|
+
|
54
|
+
# @api private
|
55
|
+
# @since 2.1.0
|
56
|
+
def call(env)
|
57
|
+
@app.call(env)
|
58
|
+
rescue Exception => exception
|
59
|
+
raise unless @config.render_errors
|
60
|
+
|
61
|
+
render_exception(env, exception)
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def render_exception(env, exception)
|
67
|
+
request = Rack::Request.new(env)
|
68
|
+
renderable = RenderableException.new(exception, responses: @config.render_error_responses)
|
69
|
+
|
70
|
+
status = renderable.status_code
|
71
|
+
request.path_info = "/#{status}"
|
72
|
+
request.set_header(Rack::REQUEST_METHOD, "GET")
|
73
|
+
|
74
|
+
@errors_app.call(request.env)
|
75
|
+
rescue Exception => failsafe_error
|
76
|
+
# rubocop:disable Style/StderrPuts
|
77
|
+
$stderr.puts "Error during exception rendering: #{failsafe_error}\n #{failsafe_error.backtrace * "\n "}"
|
78
|
+
# rubocop:enable Style/StderrPuts
|
79
|
+
|
80
|
+
[
|
81
|
+
500,
|
82
|
+
{"Content-Type" => "text/plain; charset=utf-8"},
|
83
|
+
["Internal Server Error"]
|
84
|
+
]
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# rubocop:enable Lint/RescueException
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Hanami
|
4
|
+
# @api private
|
5
|
+
module Providers
|
6
|
+
# Provider source to register routes helper component in Hanami slices.
|
7
|
+
#
|
8
|
+
# @see Hanami::Slice::RoutesHelper
|
9
|
+
#
|
10
|
+
# @api private
|
11
|
+
# @since 2.0.0
|
12
|
+
class Assets < Dry::System::Provider::Source
|
13
|
+
# @api private
|
14
|
+
def self.for_slice(slice)
|
15
|
+
Class.new(self) do |klass|
|
16
|
+
klass.instance_variable_set(:@slice, slice)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# @api private
|
21
|
+
def self.slice
|
22
|
+
@slice || Hanami.app
|
23
|
+
end
|
24
|
+
|
25
|
+
# @api private
|
26
|
+
def prepare
|
27
|
+
require "hanami/assets"
|
28
|
+
end
|
29
|
+
|
30
|
+
# @api private
|
31
|
+
def start
|
32
|
+
assets = Hanami::Assets.new(config: slice.config.assets)
|
33
|
+
|
34
|
+
register(:assets, assets)
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def slice
|
40
|
+
self.class.slice
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
data/lib/hanami/rake_tasks.rb
CHANGED
@@ -10,7 +10,7 @@ Hanami::CLI::RakeTasks.register_tasks do
|
|
10
10
|
|
11
11
|
# Ruby ecosystem compatibility
|
12
12
|
#
|
13
|
-
# Most of the SaaS automatic tasks are designed after Ruby on Rails.
|
13
|
+
# Most of the hosting SaaS automatic tasks are designed after Ruby on Rails.
|
14
14
|
# They expect the following Rake tasks to be present:
|
15
15
|
#
|
16
16
|
# * db:migrate
|
@@ -20,31 +20,32 @@ Hanami::CLI::RakeTasks.register_tasks do
|
|
20
20
|
#
|
21
21
|
# ===
|
22
22
|
#
|
23
|
-
# These Rake tasks
|
24
|
-
# want to encourage developers to use `hanami` commands.
|
23
|
+
# These Rake tasks are **NOT** listed when someone runs `rake -T`, because we
|
24
|
+
# want to encourage developers to use `hanami` CLI commands.
|
25
25
|
#
|
26
|
-
# In order to migrate the database or
|
27
|
-
# use:
|
26
|
+
# In order to migrate the database or compile assets a developer should use:
|
28
27
|
#
|
29
28
|
# * hanami db migrate
|
30
|
-
# * hanami assets
|
29
|
+
# * hanami assets compile
|
31
30
|
#
|
32
31
|
# This is the preferred way to run Hanami command line tasks.
|
33
32
|
# Please use them when you're in control of your deployment environment.
|
34
33
|
#
|
35
34
|
# If you're not in control and your deployment requires these "standard"
|
36
35
|
# Rake tasks, they are here to solve this only specific problem.
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
end
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
36
|
+
#
|
37
|
+
# namespace :db do
|
38
|
+
# task :migrate do
|
39
|
+
# # TODO(@jodosha): Enable when we'll integrate with ROM
|
40
|
+
# # run_hanami_command("db migrate")
|
41
|
+
# end
|
42
|
+
# end
|
43
|
+
|
44
|
+
if Hanami.bundled?("hanami-assets")
|
45
|
+
namespace :assets do
|
46
|
+
task :precompile do
|
47
|
+
run_hanami_command("assets compile")
|
48
|
+
end
|
48
49
|
end
|
49
50
|
end
|
50
51
|
|
@@ -53,7 +54,7 @@ Hanami::CLI::RakeTasks.register_tasks do
|
|
53
54
|
@_hanami_cli_bundler = Hanami::CLI::Bundler.new
|
54
55
|
|
55
56
|
def run_hanami_command(command)
|
56
|
-
@_hanami_cli_bundler.
|
57
|
+
@_hanami_cli_bundler.hanami_exec(command)
|
57
58
|
end
|
58
59
|
end
|
59
60
|
|
data/lib/hanami/settings.rb
CHANGED
@@ -45,7 +45,7 @@ module Hanami
|
|
45
45
|
#
|
46
46
|
# Setting values are loaded from a configurable store, which defaults to
|
47
47
|
# {Hanami::Settings::EnvStore}, which fetches the values from equivalent upper-cased keys in
|
48
|
-
# `ENV`. You can
|
48
|
+
# `ENV`. You can configure an alternative store via {Hanami::Config#settings_store}. Setting stores
|
49
49
|
# must implement a `#fetch` method with the same signature as `Hash#fetch`.
|
50
50
|
#
|
51
51
|
# [dry-c]: https://dry-rb.org/gems/dry-configurable/
|
data/lib/hanami/slice.rb
CHANGED
@@ -948,24 +948,70 @@ module Hanami
|
|
948
948
|
|
949
949
|
require_relative "slice/router"
|
950
950
|
|
951
|
+
slice = self
|
951
952
|
config = self.config
|
952
953
|
rack_monitor = self["rack.monitor"]
|
953
954
|
|
955
|
+
render_errors = render_errors?
|
956
|
+
render_detailed_errors = render_detailed_errors?
|
957
|
+
|
958
|
+
error_handlers = {}.tap do |hsh|
|
959
|
+
if render_errors || render_detailed_errors
|
960
|
+
hsh[:not_allowed] = ROUTER_NOT_ALLOWED_HANDLER
|
961
|
+
hsh[:not_found] = ROUTER_NOT_FOUND_HANDLER
|
962
|
+
end
|
963
|
+
end
|
964
|
+
|
954
965
|
Slice::Router.new(
|
955
966
|
inspector: inspector,
|
956
967
|
routes: routes,
|
957
968
|
resolver: config.router.resolver.new(slice: self),
|
969
|
+
**error_handlers,
|
958
970
|
**config.router.options
|
959
971
|
) do
|
960
972
|
use(rack_monitor)
|
961
|
-
|
962
|
-
|
973
|
+
|
974
|
+
use(
|
975
|
+
Hanami::Middleware::RenderErrors,
|
976
|
+
config,
|
977
|
+
Hanami::Middleware::PublicErrorsApp.new(slice.root.join("public"))
|
978
|
+
)
|
979
|
+
|
980
|
+
if render_detailed_errors
|
981
|
+
require "hanami/webconsole"
|
982
|
+
use(Hanami::Webconsole::Middleware, config)
|
983
|
+
end
|
984
|
+
|
985
|
+
if Hanami.bundled?("hanami-controller") && config.actions.sessions.enabled?
|
986
|
+
use(*config.actions.sessions.middleware)
|
987
|
+
end
|
988
|
+
|
989
|
+
if Hanami.bundled?("hanami-assets") && config.assets.serve
|
990
|
+
use(Hanami::Middleware::Assets)
|
963
991
|
end
|
964
992
|
|
965
993
|
middleware_stack.update(config.middleware_stack)
|
966
994
|
end
|
967
995
|
end
|
968
996
|
|
997
|
+
def render_errors?
|
998
|
+
config.render_errors
|
999
|
+
end
|
1000
|
+
|
1001
|
+
def render_detailed_errors?
|
1002
|
+
config.render_detailed_errors && Hanami.bundled?("hanami-webconsole")
|
1003
|
+
end
|
1004
|
+
|
1005
|
+
ROUTER_NOT_ALLOWED_HANDLER = -> env, allowed_http_methods {
|
1006
|
+
raise Hanami::Router::NotAllowedError.new(env, allowed_http_methods)
|
1007
|
+
}.freeze
|
1008
|
+
private_constant :ROUTER_NOT_ALLOWED_HANDLER
|
1009
|
+
|
1010
|
+
ROUTER_NOT_FOUND_HANDLER = -> env {
|
1011
|
+
raise Hanami::Router::NotFoundError.new(env)
|
1012
|
+
}.freeze
|
1013
|
+
private_constant :ROUTER_NOT_FOUND_HANDLER
|
1014
|
+
|
969
1015
|
# rubocop:enable Metrics/AbcSize
|
970
1016
|
end
|
971
1017
|
# rubocop:enable Metrics/ModuleLength
|
@@ -1,5 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative "constants"
|
3
4
|
require_relative "errors"
|
4
5
|
|
5
6
|
module Hanami
|
@@ -43,7 +44,7 @@ module Hanami
|
|
43
44
|
|
44
45
|
unless subclass.configured_for_slice?(slice)
|
45
46
|
subclass.configure_for_slice(slice)
|
46
|
-
subclass.configured_for_slices << slice
|
47
|
+
subclass.configured_for_slices << slice
|
47
48
|
end
|
48
49
|
end
|
49
50
|
end
|
@@ -58,7 +59,7 @@ module Hanami
|
|
58
59
|
|
59
60
|
slices = Hanami.app.slices.with_nested + [Hanami.app]
|
60
61
|
|
61
|
-
slices.detect { |slice| klass.name.
|
62
|
+
slices.detect { |slice| klass.name.start_with?("#{slice.namespace}#{MODULE_DELIMITER}") }
|
62
63
|
end
|
63
64
|
end
|
64
65
|
|