hanami 2.3.2 → 3.0.0.rc1
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 +55 -19
- data/LICENSE +20 -0
- data/README.md +18 -35
- data/hanami.gemspec +36 -37
- 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/slice_configured_action.rb +1 -1
- data/lib/hanami/extensions/action.rb +2 -2
- data/lib/hanami/extensions/mailer/slice_configured_mailer.rb +120 -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 +176 -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 +369 -0
- data/lib/hanami/providers/i18n/locale/en.yml +57 -0
- data/lib/hanami/providers/i18n.rb +114 -0
- 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 +71 -11
- 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 +28 -276
- 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
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "hanami/view"
|
|
4
|
+
|
|
5
|
+
module Hanami
|
|
6
|
+
module Helpers
|
|
7
|
+
# Helper methods for translating and localizing content using the slice's i18n backend.
|
|
8
|
+
#
|
|
9
|
+
# These helpers will be automatically available in your view templates, part classes, and scope
|
|
10
|
+
# classes when the `i18n` gem is bundled.
|
|
11
|
+
#
|
|
12
|
+
# @api public
|
|
13
|
+
# @since x.x.x
|
|
14
|
+
module I18nHelper
|
|
15
|
+
# Matches keys whose final segment is `html` or whose final segment ends in `_html`. These
|
|
16
|
+
# translated values are treated as HTML-safe and any string interpolation values are
|
|
17
|
+
# HTML-escaped before substitution.
|
|
18
|
+
#
|
|
19
|
+
# @api private
|
|
20
|
+
HTML_SAFE_TRANSLATION_KEY = /(\b|_)html\z/
|
|
21
|
+
|
|
22
|
+
# Translates the given key using the slice's i18n backend.
|
|
23
|
+
#
|
|
24
|
+
# When the key's final segment is `html` or ends in `_html`, the result is marked HTML-safe
|
|
25
|
+
# and any string interpolation values are HTML-escaped first.
|
|
26
|
+
#
|
|
27
|
+
# When a translation is missing and neither `:default` nor `:raise` was supplied, returns a
|
|
28
|
+
# `<span class="translation_missing">` element containing the missing key, useful for
|
|
29
|
+
# spotting missing translations during development.
|
|
30
|
+
#
|
|
31
|
+
# When the key begins with a `.`, it is treated as relative to the currently-rendering
|
|
32
|
+
# template and expanded against its name (slashes become dots; partial basenames keep
|
|
33
|
+
# their leading underscore). For example, `translate(".title")` inside
|
|
34
|
+
# `users/index.html.erb` resolves to `users.index.title`, and `translate(".label")`
|
|
35
|
+
# inside `users/_form.html.erb` resolves to `users._form.label`. Raises
|
|
36
|
+
# `I18n::ArgumentError` if called outside a template render.
|
|
37
|
+
#
|
|
38
|
+
# @param key [String, Symbol] the translation key to look up
|
|
39
|
+
# @param options [Hash] translation options forwarded to the backend (`:locale`, `:scope`,
|
|
40
|
+
# `:default`, `:count`, `:raise`, etc.), plus any interpolation values
|
|
41
|
+
#
|
|
42
|
+
# @return [String, Hanami::View::HTML::SafeString] the translated string
|
|
43
|
+
#
|
|
44
|
+
# @example Basic translation
|
|
45
|
+
# <%= translate("messages.welcome") %>
|
|
46
|
+
# # => "Welcome"
|
|
47
|
+
#
|
|
48
|
+
# @example HTML-safe translation
|
|
49
|
+
# # en.yml
|
|
50
|
+
# # greeting_html: "Hello, <strong>%{name}</strong>!"
|
|
51
|
+
# <%= translate("greeting_html", name: "<script>") %>
|
|
52
|
+
# # => "Hello, <strong><script></strong>!" (marked HTML-safe)
|
|
53
|
+
#
|
|
54
|
+
# @example Missing translation
|
|
55
|
+
# <%= translate("missing.key") %>
|
|
56
|
+
# # => '<span class="translation_missing" title="...">missing.key</span>'
|
|
57
|
+
#
|
|
58
|
+
# @example Relative key lookup
|
|
59
|
+
# # In app/templates/users/index.html.erb:
|
|
60
|
+
# <%= translate(".title") %>
|
|
61
|
+
# # Looks up "users.index.title"
|
|
62
|
+
#
|
|
63
|
+
# # In app/templates/users/_form.html.erb (a partial):
|
|
64
|
+
# <%= translate(".label") %>
|
|
65
|
+
# # Looks up "users._form.label"
|
|
66
|
+
#
|
|
67
|
+
# @api public
|
|
68
|
+
# @since x.x.x
|
|
69
|
+
def translate(key, **options)
|
|
70
|
+
key = _resolve_i18n_key(key)
|
|
71
|
+
|
|
72
|
+
html_safe = _html_safe_translation_key?(key)
|
|
73
|
+
|
|
74
|
+
options = _escape_translation_options(options) if html_safe
|
|
75
|
+
|
|
76
|
+
result =
|
|
77
|
+
if options.key?(:default) || options[:raise]
|
|
78
|
+
_context.i18n.translate(key, **options)
|
|
79
|
+
else
|
|
80
|
+
begin
|
|
81
|
+
_context.i18n.translate(key, **options, raise: true)
|
|
82
|
+
rescue ::I18n::MissingTranslationData => exception
|
|
83
|
+
return _missing_translation_markup(key, exception)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
html_safe ? result.to_s.html_safe : result
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# @api public
|
|
91
|
+
# @since x.x.x
|
|
92
|
+
alias_method :t, :translate
|
|
93
|
+
|
|
94
|
+
# Translates the given key, raising an exception if the translation is missing.
|
|
95
|
+
#
|
|
96
|
+
# @param (see #translate)
|
|
97
|
+
#
|
|
98
|
+
# @return [String, Hanami::View::HTML::SafeString] the translated string
|
|
99
|
+
#
|
|
100
|
+
# @raise [I18n::MissingTranslationData] if the translation is missing
|
|
101
|
+
#
|
|
102
|
+
# @example
|
|
103
|
+
# <%= translate!("messages.welcome") %>
|
|
104
|
+
#
|
|
105
|
+
# @api public
|
|
106
|
+
# @since x.x.x
|
|
107
|
+
def translate!(key, **options)
|
|
108
|
+
translate(key, **options, raise: true)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# @api public
|
|
112
|
+
# @since x.x.x
|
|
113
|
+
alias_method :t!, :translate!
|
|
114
|
+
|
|
115
|
+
# Localizes the given object (e.g. a date, time, or number) using the slice's i18n backend.
|
|
116
|
+
#
|
|
117
|
+
# @param object [Date, Time, DateTime, Numeric] the object to localize
|
|
118
|
+
# @param options [Hash] localization options forwarded to the backend (`:locale`, `:format`,
|
|
119
|
+
# etc.)
|
|
120
|
+
#
|
|
121
|
+
# @return [String] the localized string
|
|
122
|
+
#
|
|
123
|
+
# @example
|
|
124
|
+
# <%= localize(Date.today, format: :long) %>
|
|
125
|
+
#
|
|
126
|
+
# @api public
|
|
127
|
+
# @since x.x.x
|
|
128
|
+
def localize(object, **options)
|
|
129
|
+
_context.i18n.localize(object, **options)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# @api public
|
|
133
|
+
# @since x.x.x
|
|
134
|
+
alias_method :l, :localize
|
|
135
|
+
|
|
136
|
+
private
|
|
137
|
+
|
|
138
|
+
def _resolve_i18n_key(key)
|
|
139
|
+
return key unless key.to_s.start_with?(".")
|
|
140
|
+
|
|
141
|
+
template_name = _context.current_template_name
|
|
142
|
+
|
|
143
|
+
unless template_name
|
|
144
|
+
raise(
|
|
145
|
+
::I18n::ArgumentError,
|
|
146
|
+
"Cannot use relative translation key #{key.inspect} outside of a template render. " \
|
|
147
|
+
"Use an absolute key (without a leading dot) instead."
|
|
148
|
+
)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
"#{template_name.tr("/", ".")}#{key}"
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def _html_safe_translation_key?(key)
|
|
155
|
+
HTML_SAFE_TRANSLATION_KEY.match?(key.to_s)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def _escape_translation_options(options)
|
|
159
|
+
options.each_with_object({}) do |(key, value), result|
|
|
160
|
+
result[key] =
|
|
161
|
+
if ::I18n::RESERVED_KEYS.include?(key) || !value.is_a?(String) || value.html_safe?
|
|
162
|
+
value
|
|
163
|
+
else
|
|
164
|
+
escape_html(value)
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def _missing_translation_markup(key, error)
|
|
170
|
+
title = escape_html(error.message)
|
|
171
|
+
body = escape_html(key.to_s)
|
|
172
|
+
%(<span class="translation_missing" title="#{title}">#{body}</span>).html_safe
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
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 Gruvbox and can be customized by setting the `HANAMI_SQL_THEME`
|
|
30
|
+
# environment variable to any Rouge theme name (e.g. "github.dark", "monokai", "gruvbox.light").
|
|
31
|
+
# See `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_SQL_THEME", "gruvbox")
|
|
71
|
+
theme_class = Rouge::Theme.find(theme_name) || Rouge::Themes::Gruvbox
|
|
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
|
[
|
data/lib/hanami/providers/db.rb
CHANGED
|
@@ -7,6 +7,8 @@ require_relative "../constants"
|
|
|
7
7
|
|
|
8
8
|
module Hanami
|
|
9
9
|
module Providers
|
|
10
|
+
# rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity
|
|
11
|
+
|
|
10
12
|
# @api private
|
|
11
13
|
# @since 2.2.0
|
|
12
14
|
class DB < Hanami::Provider::Source
|
|
@@ -250,7 +252,7 @@ module Hanami
|
|
|
250
252
|
end
|
|
251
253
|
|
|
252
254
|
# Set the default gateway from ENV var without suffix
|
|
253
|
-
|
|
255
|
+
unless database_urls.key?(:default)
|
|
254
256
|
fallback_vars = ["#{env_var_prefix}DATABASE_URL", "DATABASE_URL"].uniq
|
|
255
257
|
|
|
256
258
|
fallback_vars.each do |var|
|
|
@@ -290,7 +292,9 @@ module Hanami
|
|
|
290
292
|
|
|
291
293
|
return if Hanami.bundled?(database_gem)
|
|
292
294
|
|
|
293
|
-
raise Hanami::ComponentLoadError,
|
|
295
|
+
raise Hanami::ComponentLoadError, <<~STR
|
|
296
|
+
The "#{database_gem}" gem is required to connect to #{database_url}. Please add it to your Gemfile.
|
|
297
|
+
STR
|
|
294
298
|
end
|
|
295
299
|
|
|
296
300
|
def register_rom_components(component_type, path)
|
|
@@ -309,6 +313,7 @@ module Hanami
|
|
|
309
313
|
end
|
|
310
314
|
end
|
|
311
315
|
end
|
|
316
|
+
# rubocop:enable Metrics/AbcSize, Metrics/PerceivedComplexity
|
|
312
317
|
|
|
313
318
|
Dry::System.register_provider_source(
|
|
314
319
|
:db,
|
|
@@ -3,19 +3,16 @@
|
|
|
3
3
|
module Hanami
|
|
4
4
|
module Providers
|
|
5
5
|
# @api private
|
|
6
|
-
# @since 2.2.0
|
|
7
6
|
class DBLogging < Hanami::Provider::Source
|
|
8
|
-
# @api private
|
|
9
|
-
# @since 2.2.0
|
|
10
7
|
def prepare
|
|
11
|
-
require "dry/monitor/sql/logger"
|
|
12
8
|
slice["notifications"].register_event :sql
|
|
13
9
|
end
|
|
14
10
|
|
|
15
|
-
# @api private
|
|
16
|
-
# @since 2.2.0
|
|
17
11
|
def start
|
|
18
|
-
|
|
12
|
+
require "hanami/logger/sql_logger"
|
|
13
|
+
Hanami::Logger::SQLLogger
|
|
14
|
+
.new(slice["logger"], level: slice.config.db.log_level)
|
|
15
|
+
.subscribe(slice["notifications"])
|
|
19
16
|
end
|
|
20
17
|
end
|
|
21
18
|
end
|