hanami 2.0.3 → 2.1.0.beta2
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 +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
|
|