hanami 2.3.2 → 3.0.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 +103 -19
- data/LICENSE +20 -0
- data/README.md +18 -35
- data/hanami.gemspec +36 -37
- data/lib/hanami/app.rb +12 -2
- data/lib/hanami/config/db.rb +2 -0
- data/lib/hanami/config/i18n.rb +138 -0
- data/lib/hanami/config/logger.rb +15 -7
- data/lib/hanami/config/null_config.rb +1 -1
- data/lib/hanami/config/views.rb +17 -0
- data/lib/hanami/config.rb +66 -22
- data/lib/hanami/errors.rb +6 -0
- data/lib/hanami/extensions/action/i18n_helper.rb +64 -0
- data/lib/hanami/extensions/action/name_inferrer.rb +34 -0
- data/lib/hanami/extensions/action/slice_configured_action.rb +31 -2
- data/lib/hanami/extensions/action.rb +20 -7
- data/lib/hanami/extensions/mailer/slice_configured_mailer.rb +134 -0
- data/lib/hanami/extensions/mailer.rb +28 -0
- data/lib/hanami/extensions/operation/slice_configured_db_operation.rb +2 -0
- data/lib/hanami/extensions/view/context.rb +26 -4
- data/lib/hanami/extensions/view/part.rb +2 -0
- data/lib/hanami/extensions/view/slice_configured_context.rb +7 -0
- data/lib/hanami/extensions/view/slice_configured_part.rb +2 -0
- data/lib/hanami/extensions/view/slice_configured_view.rb +8 -8
- data/lib/hanami/extensions/view/standard_helpers.rb +4 -0
- data/lib/hanami/extensions.rb +6 -1
- data/lib/hanami/helpers/assets_helper.rb +0 -4
- data/lib/hanami/helpers/form_helper.rb +1 -1
- data/lib/hanami/helpers/i18n_helper.rb +241 -0
- data/lib/hanami/logger/rack_formatter.rb +73 -0
- data/lib/hanami/logger/sql_formatter.rb +80 -0
- data/lib/hanami/logger/sql_logger.rb +48 -0
- data/lib/hanami/middleware/render_errors.rb +2 -2
- data/lib/hanami/providers/db.rb +7 -2
- data/lib/hanami/providers/db_logging.rb +4 -7
- data/lib/hanami/providers/i18n/backend.rb +373 -0
- data/lib/hanami/providers/i18n/locale/en.yml +57 -0
- data/lib/hanami/providers/i18n.rb +114 -0
- data/lib/hanami/providers/logger.rb +23 -1
- data/lib/hanami/providers/mailers.rb +101 -0
- data/lib/hanami/routes.rb +1 -0
- data/lib/hanami/settings/composite_store.rb +53 -0
- data/lib/hanami/settings.rb +4 -4
- data/lib/hanami/slice/router.rb +15 -10
- data/lib/hanami/slice.rb +102 -11
- data/lib/hanami/slice_configurable.rb +1 -3
- data/lib/hanami/slice_registrar.rb +2 -2
- data/lib/hanami/universal_logger.rb +250 -0
- data/lib/hanami/version.rb +1 -1
- data/lib/hanami/web/rack_logger.rb +2 -80
- data/lib/hanami/web/welcome.html.erb +443 -58
- data/lib/hanami.rb +4 -2
- metadata +34 -280
- data/CODE_OF_CONDUCT.md +0 -74
- data/FEATURES.md +0 -269
- data/LICENSE.md +0 -22
- data/spec/integration/action/cookies_spec.rb +0 -58
- data/spec/integration/action/csrf_protection_spec.rb +0 -54
- data/spec/integration/action/format_config_spec.rb +0 -129
- data/spec/integration/action/routes_spec.rb +0 -71
- data/spec/integration/action/sessions_spec.rb +0 -50
- data/spec/integration/action/slice_configuration_spec.rb +0 -284
- data/spec/integration/action/view_rendering/automatic_rendering_spec.rb +0 -247
- data/spec/integration/action/view_rendering/paired_view_inference_spec.rb +0 -115
- data/spec/integration/action/view_rendering/view_context_spec.rb +0 -221
- data/spec/integration/action/view_rendering_spec.rb +0 -89
- data/spec/integration/assets/assets_spec.rb +0 -155
- data/spec/integration/assets/cross_slice_assets_helpers_spec.rb +0 -129
- data/spec/integration/assets/serve_static_assets_spec.rb +0 -152
- data/spec/integration/code_loading/loading_from_app_spec.rb +0 -152
- data/spec/integration/code_loading/loading_from_lib_spec.rb +0 -242
- data/spec/integration/code_loading/loading_from_slice_spec.rb +0 -165
- data/spec/integration/container/application_routes_helper_spec.rb +0 -48
- data/spec/integration/container/auto_injection_spec.rb +0 -53
- data/spec/integration/container/auto_registration_spec.rb +0 -86
- data/spec/integration/container/autoloader_spec.rb +0 -82
- data/spec/integration/container/imports_spec.rb +0 -253
- data/spec/integration/container/prepare_container_spec.rb +0 -125
- data/spec/integration/container/provider_environment_spec.rb +0 -52
- data/spec/integration/container/provider_lifecycle_spec.rb +0 -61
- data/spec/integration/container/shutdown_spec.rb +0 -91
- data/spec/integration/container/standard_providers/rack_provider_spec.rb +0 -44
- data/spec/integration/container/standard_providers_spec.rb +0 -124
- data/spec/integration/db/auto_registration_spec.rb +0 -39
- data/spec/integration/db/commands_spec.rb +0 -80
- data/spec/integration/db/db_inflector_spec.rb +0 -57
- data/spec/integration/db/db_slices_spec.rb +0 -398
- data/spec/integration/db/db_spec.rb +0 -245
- data/spec/integration/db/gateways_spec.rb +0 -361
- data/spec/integration/db/logging_spec.rb +0 -301
- data/spec/integration/db/mappers_spec.rb +0 -84
- data/spec/integration/db/provider_config_spec.rb +0 -88
- data/spec/integration/db/provider_spec.rb +0 -35
- data/spec/integration/db/relations_spec.rb +0 -60
- data/spec/integration/db/repo_spec.rb +0 -300
- data/spec/integration/db/slices_importing_from_parent.rb +0 -130
- data/spec/integration/dotenv_loading_spec.rb +0 -138
- data/spec/integration/logging/exception_logging_spec.rb +0 -120
- data/spec/integration/logging/notifications_spec.rb +0 -68
- data/spec/integration/logging/request_logging_spec.rb +0 -202
- data/spec/integration/operations/extension_spec.rb +0 -122
- data/spec/integration/rack_app/body_parser_spec.rb +0 -108
- data/spec/integration/rack_app/method_override_spec.rb +0 -97
- data/spec/integration/rack_app/middleware_spec.rb +0 -720
- data/spec/integration/rack_app/non_booted_rack_app_spec.rb +0 -104
- data/spec/integration/rack_app/rack_app_spec.rb +0 -442
- data/spec/integration/rake_tasks_spec.rb +0 -107
- data/spec/integration/router/resource_routes_spec.rb +0 -281
- data/spec/integration/settings/access_in_slice_class_body_spec.rb +0 -83
- data/spec/integration/settings/access_to_constants_spec.rb +0 -46
- data/spec/integration/settings/loading_from_env_spec.rb +0 -188
- data/spec/integration/settings/settings_component_loading_spec.rb +0 -113
- data/spec/integration/settings/slice_registration_spec.rb +0 -145
- data/spec/integration/settings/using_types_spec.rb +0 -80
- data/spec/integration/setup_spec.rb +0 -165
- data/spec/integration/slices/external_slice_spec.rb +0 -91
- data/spec/integration/slices/slice_configuration_spec.rb +0 -42
- data/spec/integration/slices/slice_loading_spec.rb +0 -171
- data/spec/integration/slices/slice_registrations_spec.rb +0 -80
- data/spec/integration/slices/slice_routing_spec.rb +0 -219
- data/spec/integration/slices_spec.rb +0 -471
- data/spec/integration/view/config/default_context_spec.rb +0 -149
- data/spec/integration/view/config/inflector_spec.rb +0 -57
- data/spec/integration/view/config/part_class_spec.rb +0 -147
- data/spec/integration/view/config/part_namespace_spec.rb +0 -103
- data/spec/integration/view/config/paths_spec.rb +0 -119
- data/spec/integration/view/config/scope_class_spec.rb +0 -147
- data/spec/integration/view/config/scope_namespace_spec.rb +0 -103
- data/spec/integration/view/config/template_spec.rb +0 -38
- data/spec/integration/view/context/assets_spec.rb +0 -79
- data/spec/integration/view/context/inflector_spec.rb +0 -40
- data/spec/integration/view/context/request_spec.rb +0 -57
- data/spec/integration/view/context/routes_spec.rb +0 -84
- data/spec/integration/view/helpers/form_helper_spec.rb +0 -174
- data/spec/integration/view/helpers/part_helpers_spec.rb +0 -124
- data/spec/integration/view/helpers/scope_helpers_spec.rb +0 -84
- data/spec/integration/view/helpers/user_defined_helpers/part_helpers_spec.rb +0 -162
- data/spec/integration/view/helpers/user_defined_helpers/scope_helpers_spec.rb +0 -119
- data/spec/integration/view/parts/default_rendering_spec.rb +0 -138
- data/spec/integration/view/slice_configuration_spec.rb +0 -289
- data/spec/integration/view/views_spec.rb +0 -103
- data/spec/integration/web/content_security_policy_nonce_spec.rb +0 -251
- data/spec/integration/web/render_detailed_errors_spec.rb +0 -107
- data/spec/integration/web/render_errors_spec.rb +0 -242
- data/spec/integration/web/welcome_view_spec.rb +0 -84
- data/spec/spec_helper.rb +0 -28
- data/spec/support/app_integration.rb +0 -157
- data/spec/support/coverage.rb +0 -1
- data/spec/support/matchers.rb +0 -32
- data/spec/support/rspec.rb +0 -27
- data/spec/unit/hanami/config/actions/content_security_policy_spec.rb +0 -96
- data/spec/unit/hanami/config/actions/cookies_spec.rb +0 -46
- data/spec/unit/hanami/config/actions/csrf_protection_spec.rb +0 -58
- data/spec/unit/hanami/config/actions/default_values_spec.rb +0 -43
- data/spec/unit/hanami/config/actions/sessions_spec.rb +0 -48
- data/spec/unit/hanami/config/actions_spec.rb +0 -52
- data/spec/unit/hanami/config/base_url_spec.rb +0 -25
- data/spec/unit/hanami/config/console_spec.rb +0 -22
- data/spec/unit/hanami/config/db_spec.rb +0 -38
- data/spec/unit/hanami/config/inflector_spec.rb +0 -35
- data/spec/unit/hanami/config/logger_spec.rb +0 -195
- data/spec/unit/hanami/config/render_detailed_errors_spec.rb +0 -25
- data/spec/unit/hanami/config/render_errors_spec.rb +0 -25
- data/spec/unit/hanami/config/router_spec.rb +0 -44
- data/spec/unit/hanami/config/slices_spec.rb +0 -34
- data/spec/unit/hanami/config/views_spec.rb +0 -80
- data/spec/unit/hanami/env_spec.rb +0 -37
- data/spec/unit/hanami/extensions/view/context_spec.rb +0 -59
- data/spec/unit/hanami/helpers/assets_helper/asset_url_spec.rb +0 -120
- data/spec/unit/hanami/helpers/assets_helper/audio_tag_spec.rb +0 -132
- data/spec/unit/hanami/helpers/assets_helper/favicon_tag_spec.rb +0 -87
- data/spec/unit/hanami/helpers/assets_helper/image_tag_spec.rb +0 -92
- data/spec/unit/hanami/helpers/assets_helper/javascript_tag_spec.rb +0 -143
- data/spec/unit/hanami/helpers/assets_helper/stylesheet_tag_spec.rb +0 -126
- data/spec/unit/hanami/helpers/assets_helper/video_tag_spec.rb +0 -136
- data/spec/unit/hanami/helpers/form_helper_spec.rb +0 -2857
- data/spec/unit/hanami/port_spec.rb +0 -117
- data/spec/unit/hanami/providers/db/config/default_config_spec.rb +0 -100
- data/spec/unit/hanami/providers/db/config/gateway_spec.rb +0 -73
- data/spec/unit/hanami/providers/db/config_spec.rb +0 -143
- data/spec/unit/hanami/router/errors/not_allowed_error_spec.rb +0 -27
- data/spec/unit/hanami/router/errors/not_found_error_spec.rb +0 -22
- data/spec/unit/hanami/settings/env_store_spec.rb +0 -52
- data/spec/unit/hanami/settings_spec.rb +0 -111
- data/spec/unit/hanami/slice_configurable_spec.rb +0 -141
- data/spec/unit/hanami/slice_name_spec.rb +0 -47
- data/spec/unit/hanami/slice_spec.rb +0 -99
- data/spec/unit/hanami/web/rack_logger_spec.rb +0 -99
|
@@ -25,6 +25,8 @@ module Hanami
|
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
views_namespace.const_set(:Context, Class.new(context_superclass(slice)).tap { |klass|
|
|
28
|
+
# Call configure_for_slice explicitly, since this is an anonymous class at this point,
|
|
29
|
+
# so the slice cannot be inferred from its name.
|
|
28
30
|
klass.configure_for_slice(slice)
|
|
29
31
|
})
|
|
30
32
|
end
|
|
@@ -40,8 +42,8 @@ module Hanami
|
|
|
40
42
|
slice.inflector.constantize(
|
|
41
43
|
slice.inflector.camelize("#{slice.app.slice_name.name}/views/context")
|
|
42
44
|
)
|
|
43
|
-
rescue NameError =>
|
|
44
|
-
raise
|
|
45
|
+
rescue NameError => exception
|
|
46
|
+
raise exception unless %i[Views Context].include?(exception.name)
|
|
45
47
|
|
|
46
48
|
Hanami::View::Context
|
|
47
49
|
end
|
|
@@ -95,17 +97,19 @@ module Hanami
|
|
|
95
97
|
#
|
|
96
98
|
# @api private
|
|
97
99
|
# @since 2.1.0
|
|
98
|
-
def initialize(
|
|
100
|
+
def initialize(
|
|
99
101
|
inflector: nil,
|
|
100
102
|
routes: nil,
|
|
101
103
|
assets: nil,
|
|
102
104
|
request: nil,
|
|
105
|
+
i18n: nil,
|
|
103
106
|
**args
|
|
104
107
|
)
|
|
105
108
|
@inflector = inflector
|
|
106
109
|
@routes = routes
|
|
107
110
|
@assets = assets
|
|
108
111
|
@request = request
|
|
112
|
+
@i18n = i18n
|
|
109
113
|
|
|
110
114
|
@content_for = {}
|
|
111
115
|
|
|
@@ -157,7 +161,9 @@ module Hanami
|
|
|
157
161
|
# @since 2.1.0
|
|
158
162
|
def request
|
|
159
163
|
unless @request
|
|
160
|
-
raise Hanami::ComponentLoadError,
|
|
164
|
+
raise Hanami::ComponentLoadError, <<~STR
|
|
165
|
+
Request not available. Only views rendered from Hanami::Action instances have a request.
|
|
166
|
+
STR
|
|
161
167
|
end
|
|
162
168
|
|
|
163
169
|
@request
|
|
@@ -190,6 +196,22 @@ module Hanami
|
|
|
190
196
|
@routes
|
|
191
197
|
end
|
|
192
198
|
|
|
199
|
+
# Returns the slice's i18n backend.
|
|
200
|
+
#
|
|
201
|
+
# @return [Hanami::Providers::I18n::Backend] the i18n backend
|
|
202
|
+
#
|
|
203
|
+
# @raise [Hanami::ComponentLoadError] if the i18n gem is not bundled
|
|
204
|
+
#
|
|
205
|
+
# @api public
|
|
206
|
+
# @since 3.0.0
|
|
207
|
+
def i18n
|
|
208
|
+
unless @i18n
|
|
209
|
+
raise Hanami::ComponentLoadError, "the i18n gem is required to access translations"
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
@i18n
|
|
213
|
+
end
|
|
214
|
+
|
|
193
215
|
# @overload content_for(key, value = nil, &block)
|
|
194
216
|
# Stores a string or block of template markup for later use.
|
|
195
217
|
#
|
|
@@ -27,6 +27,8 @@ module Hanami
|
|
|
27
27
|
extend SliceConfiguredPart.new(slice)
|
|
28
28
|
|
|
29
29
|
const_set :PartHelpers, Class.new(PartHelpers) { |klass|
|
|
30
|
+
# Call configure_for_slice explicitly, since this is an anonymous class at this point,
|
|
31
|
+
# so the slice cannot be inferred from its name.
|
|
30
32
|
klass.configure_for_slice(slice)
|
|
31
33
|
}
|
|
32
34
|
end
|
|
@@ -39,15 +39,18 @@ module Hanami
|
|
|
39
39
|
# - the configured inflector as `inflector`
|
|
40
40
|
# - "routes" from the app container as `routes`
|
|
41
41
|
# - "assets" from the app container as `assets`
|
|
42
|
+
# - "i18n" from the slice container as `i18n`
|
|
42
43
|
def define_new
|
|
43
44
|
inflector = slice.inflector
|
|
44
45
|
resolve_routes = method(:resolve_routes)
|
|
45
46
|
resolve_assets = method(:resolve_assets)
|
|
47
|
+
resolve_i18n = method(:resolve_i18n)
|
|
46
48
|
|
|
47
49
|
define_method :new do |**kwargs|
|
|
48
50
|
kwargs[:inflector] ||= inflector
|
|
49
51
|
kwargs[:routes] ||= resolve_routes.()
|
|
50
52
|
kwargs[:assets] ||= resolve_assets.()
|
|
53
|
+
kwargs[:i18n] ||= resolve_i18n.()
|
|
51
54
|
|
|
52
55
|
super(**kwargs)
|
|
53
56
|
end
|
|
@@ -60,6 +63,10 @@ module Hanami
|
|
|
60
63
|
def resolve_assets
|
|
61
64
|
slice["assets"] if slice.key?("assets")
|
|
62
65
|
end
|
|
66
|
+
|
|
67
|
+
def resolve_i18n
|
|
68
|
+
slice["i18n"] if slice.key?("i18n")
|
|
69
|
+
end
|
|
63
70
|
end
|
|
64
71
|
end
|
|
65
72
|
end
|
|
@@ -57,6 +57,8 @@ module Hanami
|
|
|
57
57
|
define_method(:new) do |**args|
|
|
58
58
|
return super(**args) if args.key?(:rendering)
|
|
59
59
|
|
|
60
|
+
# Call configure_for_slice explicitly, since this is an anonymous class at this point,
|
|
61
|
+
# so the slice cannot be inferred from its name.
|
|
60
62
|
slice_rendering = Class.new(Hanami::View)
|
|
61
63
|
.configure_for_slice(slice)
|
|
62
64
|
.new
|
|
@@ -49,12 +49,12 @@ module Hanami
|
|
|
49
49
|
|
|
50
50
|
begin
|
|
51
51
|
slice.app.namespace.const_get(:View, false)
|
|
52
|
-
rescue NameError =>
|
|
53
|
-
raise unless
|
|
52
|
+
rescue NameError => exception
|
|
53
|
+
raise unless exception.name == :View
|
|
54
54
|
end
|
|
55
55
|
end
|
|
56
56
|
|
|
57
|
-
# rubocop:disable Metrics/AbcSize
|
|
57
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
58
58
|
def configure_view(view_class)
|
|
59
59
|
view_class.settings.each do |setting|
|
|
60
60
|
next unless slice.config.views.respond_to?(setting.name)
|
|
@@ -134,7 +134,7 @@ module Hanami
|
|
|
134
134
|
view_class.config.scope_namespace = scope_namespace
|
|
135
135
|
end
|
|
136
136
|
end
|
|
137
|
-
# rubocop:enable Metrics/AbcSize
|
|
137
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
138
138
|
|
|
139
139
|
def define_inherited
|
|
140
140
|
template_name = method(:template_name)
|
|
@@ -185,8 +185,8 @@ module Hanami
|
|
|
185
185
|
views_namespace.const_get(:Part)
|
|
186
186
|
else
|
|
187
187
|
views_namespace.const_set(:Part, Class.new(part_superclass).tap { |klass|
|
|
188
|
-
#
|
|
189
|
-
#
|
|
188
|
+
# Call configure_for_slice explicitly, since this is an anonymous class at this point,
|
|
189
|
+
# so the slice cannot be inferred from its name.
|
|
190
190
|
klass.configure_for_slice(slice)
|
|
191
191
|
})
|
|
192
192
|
end
|
|
@@ -208,8 +208,8 @@ module Hanami
|
|
|
208
208
|
views_namespace.const_get(:Scope)
|
|
209
209
|
else
|
|
210
210
|
views_namespace.const_set(:Scope, Class.new(scope_superclass).tap { |klass|
|
|
211
|
-
#
|
|
212
|
-
#
|
|
211
|
+
# Call configure_for_slice explicitly, since this is an anonymous class at this point,
|
|
212
|
+
# so the slice cannot be inferred from its name.
|
|
213
213
|
klass.configure_for_slice(slice)
|
|
214
214
|
})
|
|
215
215
|
end
|
data/lib/hanami/extensions.rb
CHANGED
|
@@ -4,7 +4,7 @@ if Hanami.bundled?("hanami-db")
|
|
|
4
4
|
require_relative "extensions/db/repo"
|
|
5
5
|
end
|
|
6
6
|
|
|
7
|
-
if Hanami.bundled?("hanami-
|
|
7
|
+
if Hanami.bundled?("hanami-action")
|
|
8
8
|
require_relative "extensions/action"
|
|
9
9
|
end
|
|
10
10
|
|
|
@@ -20,6 +20,11 @@ if Hanami.bundled?("hanami-router")
|
|
|
20
20
|
require_relative "extensions/router/errors"
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
+
if Hanami.bundled?("hanami-mailer")
|
|
24
|
+
require "hanami/mailer"
|
|
25
|
+
require_relative "extensions/mailer"
|
|
26
|
+
end
|
|
27
|
+
|
|
23
28
|
if Hanami.bundled?("dry-operation")
|
|
24
29
|
require_relative "extensions/operation"
|
|
25
30
|
end
|
|
@@ -4,8 +4,6 @@ require "uri"
|
|
|
4
4
|
require "hanami/view"
|
|
5
5
|
require_relative "../constants"
|
|
6
6
|
|
|
7
|
-
# rubocop:disable Metrics/ModuleLength
|
|
8
|
-
|
|
9
7
|
module Hanami
|
|
10
8
|
module Helpers
|
|
11
9
|
# HTML assets helpers
|
|
@@ -804,5 +802,3 @@ module Hanami
|
|
|
804
802
|
end
|
|
805
803
|
end
|
|
806
804
|
end
|
|
807
|
-
|
|
808
|
-
# rubocop:enable Metrics/ModuleLength
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "cgi/escape"
|
|
4
|
+
|
|
5
|
+
module Hanami
|
|
6
|
+
module Helpers
|
|
7
|
+
# View-layer translation and localization helpers.
|
|
8
|
+
#
|
|
9
|
+
# These helpers are automatically available in your view templates, part classes, and scope
|
|
10
|
+
# classes when the `i18n` gem is bundled. They provide `translate`/`t`, `translate!`/`t!`, and
|
|
11
|
+
# `localize`/`l`, sourcing the i18n backend from the view context and expanding relative
|
|
12
|
+
# (leading-dot) translation keys against the currently-rendering template name.
|
|
13
|
+
#
|
|
14
|
+
# The shared `translate`/`localize` logic lives in {Methods}, which is also included by the
|
|
15
|
+
# action-layer i18n helper ({Hanami::Extensions::Action::I18nHelper}). This module supplies
|
|
16
|
+
# the two view-specific concrete implementations of {Methods}'s abstract hooks: {#_i18n}
|
|
17
|
+
# returns the i18n backend from the view context, and {#_resolve_i18n_key} expands relative
|
|
18
|
+
# keys against the template name.
|
|
19
|
+
#
|
|
20
|
+
# @example Basic translation
|
|
21
|
+
# <%= translate("messages.welcome") %>
|
|
22
|
+
# # => "Welcome"
|
|
23
|
+
#
|
|
24
|
+
# @example HTML-safe translation
|
|
25
|
+
# # en.yml
|
|
26
|
+
# # greeting_html: "Hello, <strong>%{name}</strong>!"
|
|
27
|
+
# <%= translate("greeting_html", name: "<script>") %>
|
|
28
|
+
# # => "Hello, <strong><script></strong>!" (marked HTML-safe)
|
|
29
|
+
#
|
|
30
|
+
# @example Missing translation
|
|
31
|
+
# <%= translate("missing.key") %>
|
|
32
|
+
# # => '<span class="translation_missing" title="...">missing.key</span>'
|
|
33
|
+
#
|
|
34
|
+
# @example Relative key lookup
|
|
35
|
+
# # In app/templates/users/index.html.erb:
|
|
36
|
+
# <%= translate(".title") %>
|
|
37
|
+
# # Looks up "users.index.title"
|
|
38
|
+
#
|
|
39
|
+
# # In app/templates/users/_form.html.erb (a partial):
|
|
40
|
+
# <%= translate(".label") %>
|
|
41
|
+
# # Looks up "users._form.label"
|
|
42
|
+
#
|
|
43
|
+
# @api public
|
|
44
|
+
# @since 3.0.0
|
|
45
|
+
module I18nHelper
|
|
46
|
+
# Shared `translate` / `localize` (and shorthand) helper methods used by both view-layer
|
|
47
|
+
# consumers and action-layer consumers.
|
|
48
|
+
#
|
|
49
|
+
# **This module is abstract.** Including modules must override two private hooks:
|
|
50
|
+
#
|
|
51
|
+
# - {#_i18n} — returns the i18n backend to delegate to.
|
|
52
|
+
# - {#_resolve_i18n_key} — expands relative (leading-dot) keys against the consumer's
|
|
53
|
+
# context, and is a no-op for absolute keys.
|
|
54
|
+
#
|
|
55
|
+
# @api public
|
|
56
|
+
# @since 3.0.0
|
|
57
|
+
module Methods
|
|
58
|
+
# Matches keys whose final segment is `html` or whose final segment ends in `_html`. These
|
|
59
|
+
# translated values are treated as HTML-safe and any string interpolation values are
|
|
60
|
+
# HTML-escaped before substitution.
|
|
61
|
+
#
|
|
62
|
+
# @api private
|
|
63
|
+
HTML_SAFE_TRANSLATION_KEY = /(\b|_)html\z/
|
|
64
|
+
|
|
65
|
+
# Translates the given key using the slice's i18n backend.
|
|
66
|
+
#
|
|
67
|
+
# When the key's final segment is `html` or ends in `_html`, the result is marked HTML-safe
|
|
68
|
+
# and any string interpolation values are HTML-escaped first.
|
|
69
|
+
#
|
|
70
|
+
# When a translation is missing and neither `:default` nor `:raise` was supplied, returns a
|
|
71
|
+
# `<span class="translation_missing">` element containing the missing key, useful for
|
|
72
|
+
# spotting missing translations during development.
|
|
73
|
+
#
|
|
74
|
+
# When the key begins with a `.`, it is treated as relative to the consumer's context and
|
|
75
|
+
# expanded by {#_resolve_i18n_key}.
|
|
76
|
+
#
|
|
77
|
+
# @param key [String, Symbol] the translation key to look up
|
|
78
|
+
# @param options [Hash] translation options forwarded to the backend (`:locale`, `:scope`,
|
|
79
|
+
# `:default`, `:count`, `:raise`, etc.), plus any interpolation values
|
|
80
|
+
#
|
|
81
|
+
# @return [String] the translated string (marked HTML-safe when hanami-view is bundled and
|
|
82
|
+
# the key ends in `_html` or `html`)
|
|
83
|
+
#
|
|
84
|
+
# @api public
|
|
85
|
+
# @since 3.0.0
|
|
86
|
+
def translate(key, **options)
|
|
87
|
+
key = _resolve_i18n_key(key)
|
|
88
|
+
|
|
89
|
+
html_safe = _html_safe_translation_key?(key)
|
|
90
|
+
|
|
91
|
+
options = _escape_translation_options(options) if html_safe
|
|
92
|
+
|
|
93
|
+
result =
|
|
94
|
+
if options.key?(:default) || options[:raise]
|
|
95
|
+
_i18n.translate(key, **options)
|
|
96
|
+
else
|
|
97
|
+
begin
|
|
98
|
+
_i18n.translate(key, **options, raise: true)
|
|
99
|
+
rescue ::I18n::MissingTranslationData => exception
|
|
100
|
+
return _missing_translation_markup(key, exception)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
html_safe ? _i18n_mark_html_safe(result.to_s) : result
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# @api public
|
|
108
|
+
# @since 3.0.0
|
|
109
|
+
alias_method :t, :translate
|
|
110
|
+
|
|
111
|
+
# Translates the given key, raising an exception if the translation is missing.
|
|
112
|
+
#
|
|
113
|
+
# @param (see #translate)
|
|
114
|
+
#
|
|
115
|
+
# @return [String] the translated string (marked HTML-safe when hanami-view is bundled and
|
|
116
|
+
# the key ends in `_html` or `html`)
|
|
117
|
+
#
|
|
118
|
+
# @raise [I18n::MissingTranslationData] if the translation is missing
|
|
119
|
+
#
|
|
120
|
+
# @api public
|
|
121
|
+
# @since 3.0.0
|
|
122
|
+
def translate!(key, **options)
|
|
123
|
+
translate(key, **options, raise: true)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# @api public
|
|
127
|
+
# @since 3.0.0
|
|
128
|
+
alias_method :t!, :translate!
|
|
129
|
+
|
|
130
|
+
# Localizes the given object (e.g. a date, time, or number) using the slice's i18n backend.
|
|
131
|
+
#
|
|
132
|
+
# @param object [Date, Time, DateTime, Numeric] the object to localize
|
|
133
|
+
# @param options [Hash] localization options forwarded to the backend (`:locale`, `:format`,
|
|
134
|
+
# etc.)
|
|
135
|
+
#
|
|
136
|
+
# @return [String] the localized string
|
|
137
|
+
#
|
|
138
|
+
# @api public
|
|
139
|
+
# @since 3.0.0
|
|
140
|
+
def localize(object, **options)
|
|
141
|
+
_i18n.localize(object, **options)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# @api public
|
|
145
|
+
# @since 3.0.0
|
|
146
|
+
alias_method :l, :localize
|
|
147
|
+
|
|
148
|
+
private
|
|
149
|
+
|
|
150
|
+
# Returns the i18n backend the helper methods delegate to.
|
|
151
|
+
#
|
|
152
|
+
# Including modules must override this method to return an I18n backend instance.
|
|
153
|
+
#
|
|
154
|
+
# @return [Hanami::Providers::I18n::Backend]
|
|
155
|
+
def _i18n
|
|
156
|
+
raise NoMethodError, "#{self.class} must implement #_i18n to return an i18n backend"
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Resolves the given key, returning it unchanged by default.
|
|
160
|
+
#
|
|
161
|
+
# Override this hook to expand relative (leading-dot) keys against a context.
|
|
162
|
+
#
|
|
163
|
+
# @param key [String, Symbol]
|
|
164
|
+
#
|
|
165
|
+
# @return [String, Symbol] the resolved key
|
|
166
|
+
def _resolve_i18n_key(key)
|
|
167
|
+
key
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def _html_safe_translation_key?(key)
|
|
171
|
+
HTML_SAFE_TRANSLATION_KEY.match?(key.to_s)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def _escape_translation_options(options)
|
|
175
|
+
options.each_with_object({}) do |(key, value), result|
|
|
176
|
+
result[key] =
|
|
177
|
+
if ::I18n::RESERVED_KEYS.include?(key) || !value.is_a?(String) || _i18n_html_safe?(value)
|
|
178
|
+
value
|
|
179
|
+
else
|
|
180
|
+
_i18n_html_escape(value)
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def _missing_translation_markup(key, error)
|
|
186
|
+
title = _i18n_html_escape(error.message)
|
|
187
|
+
body = _i18n_html_escape(key.to_s)
|
|
188
|
+
_i18n_mark_html_safe(%(<span class="translation_missing" title="#{title}">#{body}</span>))
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Escapes `value` for HTML. Prefers `Hanami::View::Helpers::EscapeHelper#escape_html` when
|
|
192
|
+
# Hanami View's EscapeHelper is included, falling back to stdlib `CGI.escapeHTML` so the
|
|
193
|
+
# helper works in API-only apps that don't bundle hanami-view.
|
|
194
|
+
def _i18n_html_escape(value)
|
|
195
|
+
if respond_to?(:escape_html)
|
|
196
|
+
escape_html(value)
|
|
197
|
+
else
|
|
198
|
+
CGI.escapeHTML(value.to_s)
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Marks the given string as HTML-safe when Hanami View is loaded, otherwise returns the
|
|
203
|
+
# string unchanged.
|
|
204
|
+
#
|
|
205
|
+
# The HTML-safe marker is only meaningful to Hanami View's template rendering, so there's
|
|
206
|
+
# nothing to do in its absence.
|
|
207
|
+
def _i18n_mark_html_safe(str)
|
|
208
|
+
str.respond_to?(:html_safe) ? str.html_safe : str
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def _i18n_html_safe?(value)
|
|
212
|
+
value.respond_to?(:html_safe?) && value.html_safe?
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
include Methods
|
|
217
|
+
|
|
218
|
+
private
|
|
219
|
+
|
|
220
|
+
def _i18n
|
|
221
|
+
_context.i18n
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def _resolve_i18n_key(key)
|
|
225
|
+
return key unless key.to_s.start_with?(".")
|
|
226
|
+
|
|
227
|
+
template_name = _context.current_template_name
|
|
228
|
+
|
|
229
|
+
unless template_name
|
|
230
|
+
raise(
|
|
231
|
+
::I18n::ArgumentError,
|
|
232
|
+
"Cannot use relative translation key #{key.inspect} outside of a template render. " \
|
|
233
|
+
"Use an absolute key (without a leading dot) instead."
|
|
234
|
+
)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
"#{template_name.tr("/", ".")}#{key}"
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "dry/logger"
|
|
4
|
+
|
|
5
|
+
module Hanami
|
|
6
|
+
module Logger
|
|
7
|
+
# Rack request log formatter for Dry Logger.
|
|
8
|
+
#
|
|
9
|
+
# Formats rack request log entries with colorized output for HTTP verbs, status codes, and
|
|
10
|
+
# request paths, making it easier to visually scan logs in development.
|
|
11
|
+
#
|
|
12
|
+
# HTTP verbs each have a distinct color. Status codes follow traffic-light coloring (2xx green,
|
|
13
|
+
# 3xx cyan, 4xx yellow, 5xx red), and the request path echoes the status color so both signals
|
|
14
|
+
# reinforce each other at a glance.
|
|
15
|
+
#
|
|
16
|
+
# Colorization is only active when `colorize: true` is set in the logger options (the default
|
|
17
|
+
# in development).
|
|
18
|
+
#
|
|
19
|
+
# @api private
|
|
20
|
+
class RackFormatter < Dry::Logger::Formatters::String
|
|
21
|
+
RACK_TEMPLATE = <<~TEXT
|
|
22
|
+
[%<progname>s] [%<severity>s] [%<time>s] \
|
|
23
|
+
%<verb>s %<status>s %<elapsed>s %<ip>s %<path>s %<length>s %<payload>s
|
|
24
|
+
%<params>s
|
|
25
|
+
TEXT
|
|
26
|
+
|
|
27
|
+
VERB_COLORS = {
|
|
28
|
+
"GET" => :green,
|
|
29
|
+
"POST" => :yellow,
|
|
30
|
+
"PUT" => :blue,
|
|
31
|
+
"PATCH" => :blue,
|
|
32
|
+
"DELETE" => :red,
|
|
33
|
+
"HEAD" => :cyan
|
|
34
|
+
}.freeze
|
|
35
|
+
|
|
36
|
+
Colors = Dry::Logger::Formatters::Colors
|
|
37
|
+
private_constant :Colors
|
|
38
|
+
|
|
39
|
+
def initialize(**options)
|
|
40
|
+
super
|
|
41
|
+
@template = Dry::Logger::Formatters::Template[RACK_TEMPLATE]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def format_values(entry)
|
|
47
|
+
return super unless colorize?
|
|
48
|
+
|
|
49
|
+
status_color = status_color(entry.to_h[:status])
|
|
50
|
+
|
|
51
|
+
super.merge(
|
|
52
|
+
verb: Colors.call(VERB_COLORS.fetch(entry.to_h[:verb].to_s.upcase, :gray), entry.to_h[:verb]),
|
|
53
|
+
status: Colors.call(status_color, entry.to_h[:status]),
|
|
54
|
+
path: Colors.call(status_color, entry.to_h[:path])
|
|
55
|
+
)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def format_params(value)
|
|
59
|
+
value unless value.empty?
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def status_color(status)
|
|
63
|
+
case status.to_i
|
|
64
|
+
when 200..299 then :green
|
|
65
|
+
when 300..399 then :cyan
|
|
66
|
+
when 400..499 then :yellow
|
|
67
|
+
when 500..599 then :red
|
|
68
|
+
else :gray
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "dry/logger"
|
|
4
|
+
|
|
5
|
+
module Hanami
|
|
6
|
+
module Logger
|
|
7
|
+
# SQL query log formatter for Dry Logger.
|
|
8
|
+
#
|
|
9
|
+
# Formats SQL query log entries with a template that mirrors the structure of the built-in rack
|
|
10
|
+
# log formatter, providing consistent visual formatting across both HTTP request and database
|
|
11
|
+
# query logs.
|
|
12
|
+
#
|
|
13
|
+
# For example, in development, SQL logs alongside Rack logs:
|
|
14
|
+
#
|
|
15
|
+
# [my_app] [INFO] [2026-03-04 10:15:32] SQL sqlite 1.234ms SELECT * FROM users
|
|
16
|
+
# [my_app] [INFO] [2026-03-04 10:15:32] GET 200 1ms 127.0.0.1 /users -
|
|
17
|
+
#
|
|
18
|
+
# In production, the default JSON formatter handles SQL entries automatically via the structured
|
|
19
|
+
# payload.
|
|
20
|
+
#
|
|
21
|
+
# Supports colorization via Dry Logger's template color tags. When `colorize: true` is set in
|
|
22
|
+
# the logger options (the default in development), the "SQL" label is colorized, and severity
|
|
23
|
+
# is colorized per-level by the parent formatter (e.g. INFO => magenta, ERROR => red).
|
|
24
|
+
#
|
|
25
|
+
# When colorization is enabled and the "rouge" gem is available, SQL queries are syntax
|
|
26
|
+
# highlighted using Rouge's SQL lexer. This is a soft dependency; if Rouge is not installed,
|
|
27
|
+
# queries output as plain, unhighlighted text.
|
|
28
|
+
#
|
|
29
|
+
# The Rouge theme defaults to "pastie" and can be customized by setting the
|
|
30
|
+
# `HANAMI_LOG_SYNTAX_THEME` environment variable to any Rouge theme name. See
|
|
31
|
+
# `Rouge::Theme.registry` for available themes.
|
|
32
|
+
#
|
|
33
|
+
# @see Hanami::Logger::SQLLogger
|
|
34
|
+
#
|
|
35
|
+
# @api private
|
|
36
|
+
class SQLFormatter < Dry::Logger::Formatters::String
|
|
37
|
+
SQL_TEMPLATE = <<~TEXT
|
|
38
|
+
[%<progname>s] [%<severity>s] [%<time>s] SQL %<db>s %<elapsed>s%<elapsed_unit>s %<query>s
|
|
39
|
+
TEXT
|
|
40
|
+
|
|
41
|
+
SQL_TEMPLATE_COLORIZED = <<~TEXT
|
|
42
|
+
[%<progname>s] [%<severity>s] [%<time>s] <blue>SQL</blue> %<db>s %<elapsed>s%<elapsed_unit>s %<query>s
|
|
43
|
+
TEXT
|
|
44
|
+
|
|
45
|
+
def initialize(**options)
|
|
46
|
+
super
|
|
47
|
+
@template = Dry::Logger::Formatters::Template[
|
|
48
|
+
colorize? ? SQL_TEMPLATE_COLORIZED : SQL_TEMPLATE
|
|
49
|
+
]
|
|
50
|
+
@sql_colorizer = build_sql_colorizer if colorize?
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def format_query(value)
|
|
56
|
+
if @sql_colorizer
|
|
57
|
+
@sql_colorizer.call(value)
|
|
58
|
+
else
|
|
59
|
+
value
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def build_sql_colorizer
|
|
64
|
+
begin
|
|
65
|
+
require "rouge"
|
|
66
|
+
rescue LoadError
|
|
67
|
+
return nil
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
theme_name = ENV.fetch("HANAMI_LOG_SYNTAX_THEME", "pastie")
|
|
71
|
+
theme_class = Rouge::Theme.find(theme_name) || Rouge::Themes::Pastie
|
|
72
|
+
formatter = Rouge::Formatters::Terminal256.new(theme_class.new)
|
|
73
|
+
|
|
74
|
+
lexer = Rouge::Lexers::SQL.new
|
|
75
|
+
|
|
76
|
+
->(sql) { formatter.format(lexer.lex(sql)) }
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hanami
|
|
4
|
+
module Logger
|
|
5
|
+
# SQL query logger that integrates with the Hanami logger using structured, tagged logging.
|
|
6
|
+
#
|
|
7
|
+
# Subscribes to `:sql` notification events (emitted by ROM via the Hanami app's
|
|
8
|
+
# `"notifications"` component) and logs each query as a structured payload. The log entries are
|
|
9
|
+
# tagged as `:sql`, which allows Dry Logger backends to route them to a dedicated formatter (see
|
|
10
|
+
# {SQLFormatter}), in the same way that rack log entries are routed using the `:rack` tag.
|
|
11
|
+
#
|
|
12
|
+
# @see SQLFormatter
|
|
13
|
+
# @see Hanami::Providers::DBLogging
|
|
14
|
+
#
|
|
15
|
+
# @api private
|
|
16
|
+
class SQLLogger
|
|
17
|
+
attr_reader :logger, :level
|
|
18
|
+
|
|
19
|
+
# @param logger [#tagged, #info] a Hanami-compatible logger (typically a
|
|
20
|
+
# Dry::Logger::Dispatcher or a {Hanami::UniversalLogger}-wrapped logger)
|
|
21
|
+
def initialize(logger, level: :debug)
|
|
22
|
+
@logger = logger
|
|
23
|
+
@level = level
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Subscribes to `:sql` notification events.
|
|
27
|
+
#
|
|
28
|
+
# @param notifications [Dry::Monitor::Notifications] the notifications bus
|
|
29
|
+
# @return [void]
|
|
30
|
+
def subscribe(notifications)
|
|
31
|
+
notifications.subscribe(:sql) { |params| log_query(**params) }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Log a SQL query with structured data.
|
|
35
|
+
#
|
|
36
|
+
# @param time [Numeric] elapsed time in milliseconds
|
|
37
|
+
# @param name [Symbol] database adapter name (e.g. `:sqlite`, `:postgres`)
|
|
38
|
+
# @param query [String] the SQL query string
|
|
39
|
+
def log_query(time:, name:, query:)
|
|
40
|
+
logger.tagged(:sql) do
|
|
41
|
+
logger.public_send(@level) do
|
|
42
|
+
{query:, db: name, elapsed: time.round(3), elapsed_unit: "ms"}
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -72,9 +72,9 @@ module Hanami
|
|
|
72
72
|
request.set_header(Rack::REQUEST_METHOD, "GET")
|
|
73
73
|
|
|
74
74
|
@errors_app.call(request.env)
|
|
75
|
-
rescue Exception =>
|
|
75
|
+
rescue Exception => exception
|
|
76
76
|
# rubocop:disable Style/StderrPuts
|
|
77
|
-
$stderr.puts "Error during exception rendering: #{
|
|
77
|
+
$stderr.puts "Error during exception rendering: #{exception}\n #{exception.backtrace * "\n "}"
|
|
78
78
|
# rubocop:enable Style/StderrPuts
|
|
79
79
|
|
|
80
80
|
[
|