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.
Files changed (184) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +55 -19
  3. data/LICENSE +20 -0
  4. data/README.md +18 -35
  5. data/hanami.gemspec +36 -37
  6. data/lib/hanami/config/db.rb +2 -0
  7. data/lib/hanami/config/i18n.rb +138 -0
  8. data/lib/hanami/config/logger.rb +15 -7
  9. data/lib/hanami/config/null_config.rb +1 -1
  10. data/lib/hanami/config/views.rb +17 -0
  11. data/lib/hanami/config.rb +66 -22
  12. data/lib/hanami/errors.rb +6 -0
  13. data/lib/hanami/extensions/action/slice_configured_action.rb +1 -1
  14. data/lib/hanami/extensions/action.rb +2 -2
  15. data/lib/hanami/extensions/mailer/slice_configured_mailer.rb +120 -0
  16. data/lib/hanami/extensions/mailer.rb +28 -0
  17. data/lib/hanami/extensions/operation/slice_configured_db_operation.rb +2 -0
  18. data/lib/hanami/extensions/view/context.rb +26 -4
  19. data/lib/hanami/extensions/view/part.rb +2 -0
  20. data/lib/hanami/extensions/view/slice_configured_context.rb +7 -0
  21. data/lib/hanami/extensions/view/slice_configured_part.rb +2 -0
  22. data/lib/hanami/extensions/view/slice_configured_view.rb +8 -8
  23. data/lib/hanami/extensions/view/standard_helpers.rb +4 -0
  24. data/lib/hanami/extensions.rb +6 -1
  25. data/lib/hanami/helpers/assets_helper.rb +0 -4
  26. data/lib/hanami/helpers/form_helper.rb +1 -1
  27. data/lib/hanami/helpers/i18n_helper.rb +176 -0
  28. data/lib/hanami/logger/rack_formatter.rb +73 -0
  29. data/lib/hanami/logger/sql_formatter.rb +80 -0
  30. data/lib/hanami/logger/sql_logger.rb +48 -0
  31. data/lib/hanami/middleware/render_errors.rb +2 -2
  32. data/lib/hanami/providers/db.rb +7 -2
  33. data/lib/hanami/providers/db_logging.rb +4 -7
  34. data/lib/hanami/providers/i18n/backend.rb +369 -0
  35. data/lib/hanami/providers/i18n/locale/en.yml +57 -0
  36. data/lib/hanami/providers/i18n.rb +114 -0
  37. data/lib/hanami/providers/mailers.rb +101 -0
  38. data/lib/hanami/routes.rb +1 -0
  39. data/lib/hanami/settings/composite_store.rb +53 -0
  40. data/lib/hanami/settings.rb +4 -4
  41. data/lib/hanami/slice/router.rb +15 -10
  42. data/lib/hanami/slice.rb +71 -11
  43. data/lib/hanami/slice_registrar.rb +2 -2
  44. data/lib/hanami/universal_logger.rb +250 -0
  45. data/lib/hanami/version.rb +1 -1
  46. data/lib/hanami/web/rack_logger.rb +2 -80
  47. data/lib/hanami/web/welcome.html.erb +443 -58
  48. data/lib/hanami.rb +4 -2
  49. metadata +28 -276
  50. data/CODE_OF_CONDUCT.md +0 -74
  51. data/FEATURES.md +0 -269
  52. data/LICENSE.md +0 -22
  53. data/spec/integration/action/cookies_spec.rb +0 -58
  54. data/spec/integration/action/csrf_protection_spec.rb +0 -54
  55. data/spec/integration/action/format_config_spec.rb +0 -129
  56. data/spec/integration/action/routes_spec.rb +0 -71
  57. data/spec/integration/action/sessions_spec.rb +0 -50
  58. data/spec/integration/action/slice_configuration_spec.rb +0 -284
  59. data/spec/integration/action/view_rendering/automatic_rendering_spec.rb +0 -247
  60. data/spec/integration/action/view_rendering/paired_view_inference_spec.rb +0 -115
  61. data/spec/integration/action/view_rendering/view_context_spec.rb +0 -221
  62. data/spec/integration/action/view_rendering_spec.rb +0 -89
  63. data/spec/integration/assets/assets_spec.rb +0 -155
  64. data/spec/integration/assets/cross_slice_assets_helpers_spec.rb +0 -129
  65. data/spec/integration/assets/serve_static_assets_spec.rb +0 -152
  66. data/spec/integration/code_loading/loading_from_app_spec.rb +0 -152
  67. data/spec/integration/code_loading/loading_from_lib_spec.rb +0 -242
  68. data/spec/integration/code_loading/loading_from_slice_spec.rb +0 -165
  69. data/spec/integration/container/application_routes_helper_spec.rb +0 -48
  70. data/spec/integration/container/auto_injection_spec.rb +0 -53
  71. data/spec/integration/container/auto_registration_spec.rb +0 -86
  72. data/spec/integration/container/autoloader_spec.rb +0 -82
  73. data/spec/integration/container/imports_spec.rb +0 -253
  74. data/spec/integration/container/prepare_container_spec.rb +0 -125
  75. data/spec/integration/container/provider_environment_spec.rb +0 -52
  76. data/spec/integration/container/provider_lifecycle_spec.rb +0 -61
  77. data/spec/integration/container/shutdown_spec.rb +0 -91
  78. data/spec/integration/container/standard_providers/rack_provider_spec.rb +0 -44
  79. data/spec/integration/container/standard_providers_spec.rb +0 -124
  80. data/spec/integration/db/auto_registration_spec.rb +0 -39
  81. data/spec/integration/db/commands_spec.rb +0 -80
  82. data/spec/integration/db/db_inflector_spec.rb +0 -57
  83. data/spec/integration/db/db_slices_spec.rb +0 -398
  84. data/spec/integration/db/db_spec.rb +0 -245
  85. data/spec/integration/db/gateways_spec.rb +0 -361
  86. data/spec/integration/db/logging_spec.rb +0 -301
  87. data/spec/integration/db/mappers_spec.rb +0 -84
  88. data/spec/integration/db/provider_config_spec.rb +0 -88
  89. data/spec/integration/db/provider_spec.rb +0 -35
  90. data/spec/integration/db/relations_spec.rb +0 -60
  91. data/spec/integration/db/repo_spec.rb +0 -300
  92. data/spec/integration/db/slices_importing_from_parent.rb +0 -130
  93. data/spec/integration/dotenv_loading_spec.rb +0 -138
  94. data/spec/integration/logging/exception_logging_spec.rb +0 -120
  95. data/spec/integration/logging/notifications_spec.rb +0 -68
  96. data/spec/integration/logging/request_logging_spec.rb +0 -202
  97. data/spec/integration/operations/extension_spec.rb +0 -122
  98. data/spec/integration/rack_app/body_parser_spec.rb +0 -108
  99. data/spec/integration/rack_app/method_override_spec.rb +0 -97
  100. data/spec/integration/rack_app/middleware_spec.rb +0 -720
  101. data/spec/integration/rack_app/non_booted_rack_app_spec.rb +0 -104
  102. data/spec/integration/rack_app/rack_app_spec.rb +0 -442
  103. data/spec/integration/rake_tasks_spec.rb +0 -107
  104. data/spec/integration/router/resource_routes_spec.rb +0 -281
  105. data/spec/integration/settings/access_in_slice_class_body_spec.rb +0 -83
  106. data/spec/integration/settings/access_to_constants_spec.rb +0 -46
  107. data/spec/integration/settings/loading_from_env_spec.rb +0 -188
  108. data/spec/integration/settings/settings_component_loading_spec.rb +0 -113
  109. data/spec/integration/settings/slice_registration_spec.rb +0 -145
  110. data/spec/integration/settings/using_types_spec.rb +0 -80
  111. data/spec/integration/setup_spec.rb +0 -165
  112. data/spec/integration/slices/external_slice_spec.rb +0 -91
  113. data/spec/integration/slices/slice_configuration_spec.rb +0 -42
  114. data/spec/integration/slices/slice_loading_spec.rb +0 -171
  115. data/spec/integration/slices/slice_registrations_spec.rb +0 -80
  116. data/spec/integration/slices/slice_routing_spec.rb +0 -219
  117. data/spec/integration/slices_spec.rb +0 -471
  118. data/spec/integration/view/config/default_context_spec.rb +0 -149
  119. data/spec/integration/view/config/inflector_spec.rb +0 -57
  120. data/spec/integration/view/config/part_class_spec.rb +0 -147
  121. data/spec/integration/view/config/part_namespace_spec.rb +0 -103
  122. data/spec/integration/view/config/paths_spec.rb +0 -119
  123. data/spec/integration/view/config/scope_class_spec.rb +0 -147
  124. data/spec/integration/view/config/scope_namespace_spec.rb +0 -103
  125. data/spec/integration/view/config/template_spec.rb +0 -38
  126. data/spec/integration/view/context/assets_spec.rb +0 -79
  127. data/spec/integration/view/context/inflector_spec.rb +0 -40
  128. data/spec/integration/view/context/request_spec.rb +0 -57
  129. data/spec/integration/view/context/routes_spec.rb +0 -84
  130. data/spec/integration/view/helpers/form_helper_spec.rb +0 -174
  131. data/spec/integration/view/helpers/part_helpers_spec.rb +0 -124
  132. data/spec/integration/view/helpers/scope_helpers_spec.rb +0 -84
  133. data/spec/integration/view/helpers/user_defined_helpers/part_helpers_spec.rb +0 -162
  134. data/spec/integration/view/helpers/user_defined_helpers/scope_helpers_spec.rb +0 -119
  135. data/spec/integration/view/parts/default_rendering_spec.rb +0 -138
  136. data/spec/integration/view/slice_configuration_spec.rb +0 -289
  137. data/spec/integration/view/views_spec.rb +0 -103
  138. data/spec/integration/web/content_security_policy_nonce_spec.rb +0 -251
  139. data/spec/integration/web/render_detailed_errors_spec.rb +0 -107
  140. data/spec/integration/web/render_errors_spec.rb +0 -242
  141. data/spec/integration/web/welcome_view_spec.rb +0 -84
  142. data/spec/spec_helper.rb +0 -28
  143. data/spec/support/app_integration.rb +0 -157
  144. data/spec/support/coverage.rb +0 -1
  145. data/spec/support/matchers.rb +0 -32
  146. data/spec/support/rspec.rb +0 -27
  147. data/spec/unit/hanami/config/actions/content_security_policy_spec.rb +0 -96
  148. data/spec/unit/hanami/config/actions/cookies_spec.rb +0 -46
  149. data/spec/unit/hanami/config/actions/csrf_protection_spec.rb +0 -58
  150. data/spec/unit/hanami/config/actions/default_values_spec.rb +0 -43
  151. data/spec/unit/hanami/config/actions/sessions_spec.rb +0 -48
  152. data/spec/unit/hanami/config/actions_spec.rb +0 -52
  153. data/spec/unit/hanami/config/base_url_spec.rb +0 -25
  154. data/spec/unit/hanami/config/console_spec.rb +0 -22
  155. data/spec/unit/hanami/config/db_spec.rb +0 -38
  156. data/spec/unit/hanami/config/inflector_spec.rb +0 -35
  157. data/spec/unit/hanami/config/logger_spec.rb +0 -195
  158. data/spec/unit/hanami/config/render_detailed_errors_spec.rb +0 -25
  159. data/spec/unit/hanami/config/render_errors_spec.rb +0 -25
  160. data/spec/unit/hanami/config/router_spec.rb +0 -44
  161. data/spec/unit/hanami/config/slices_spec.rb +0 -34
  162. data/spec/unit/hanami/config/views_spec.rb +0 -80
  163. data/spec/unit/hanami/env_spec.rb +0 -37
  164. data/spec/unit/hanami/extensions/view/context_spec.rb +0 -59
  165. data/spec/unit/hanami/helpers/assets_helper/asset_url_spec.rb +0 -120
  166. data/spec/unit/hanami/helpers/assets_helper/audio_tag_spec.rb +0 -132
  167. data/spec/unit/hanami/helpers/assets_helper/favicon_tag_spec.rb +0 -87
  168. data/spec/unit/hanami/helpers/assets_helper/image_tag_spec.rb +0 -92
  169. data/spec/unit/hanami/helpers/assets_helper/javascript_tag_spec.rb +0 -143
  170. data/spec/unit/hanami/helpers/assets_helper/stylesheet_tag_spec.rb +0 -126
  171. data/spec/unit/hanami/helpers/assets_helper/video_tag_spec.rb +0 -136
  172. data/spec/unit/hanami/helpers/form_helper_spec.rb +0 -2857
  173. data/spec/unit/hanami/port_spec.rb +0 -117
  174. data/spec/unit/hanami/providers/db/config/default_config_spec.rb +0 -100
  175. data/spec/unit/hanami/providers/db/config/gateway_spec.rb +0 -73
  176. data/spec/unit/hanami/providers/db/config_spec.rb +0 -143
  177. data/spec/unit/hanami/router/errors/not_allowed_error_spec.rb +0 -27
  178. data/spec/unit/hanami/router/errors/not_found_error_spec.rb +0 -22
  179. data/spec/unit/hanami/settings/env_store_spec.rb +0 -52
  180. data/spec/unit/hanami/settings_spec.rb +0 -111
  181. data/spec/unit/hanami/slice_configurable_spec.rb +0 -141
  182. data/spec/unit/hanami/slice_name_spec.rb +0 -47
  183. data/spec/unit/hanami/slice_spec.rb +0 -99
  184. 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>&lt;script&gt;</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 => failsafe_error
75
+ rescue Exception => exception
76
76
  # rubocop:disable Style/StderrPuts
77
- $stderr.puts "Error during exception rendering: #{failsafe_error}\n #{failsafe_error.backtrace * "\n "}"
77
+ $stderr.puts "Error during exception rendering: #{exception}\n #{exception.backtrace * "\n "}"
78
78
  # rubocop:enable Style/StderrPuts
79
79
 
80
80
  [
@@ -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
- if !database_urls.key?(:default)
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, %(The "#{database_gem}" gem is required to connect to #{database_url}. Please add it to your Gemfile.)
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
- Dry::Monitor::SQL::Logger.new(slice["logger"]).subscribe(slice["notifications"])
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