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
@@ -140,8 +140,8 @@ module Hanami
140
140
 
141
141
  begin
142
142
  require slice_settings_require_path
143
- rescue LoadError => e
144
- raise e unless e.path == slice_settings_require_path
143
+ rescue LoadError => exception
144
+ raise exception unless exception.path == slice_settings_require_path
145
145
  end
146
146
  end
147
147
  end
@@ -167,8 +167,8 @@ module Hanami
167
167
  else
168
168
  public_send("#{name}=", value)
169
169
  end
170
- rescue => e # rubocop:disable Style/RescueStandardError
171
- errs[name] = e
170
+ rescue => exception # rubocop:disable Style/RescueStandardError
171
+ errs[name] = exception
172
172
  end
173
173
 
174
174
  raise InvalidSettingsError, errors if errors.any?
@@ -23,7 +23,14 @@ module Hanami
23
23
  attr_reader :path_prefix
24
24
 
25
25
  # @api private
26
- def initialize(routes:, inflector:, middleware_stack: Routing::Middleware::Stack.new, prefix: ::Hanami::Router::DEFAULT_PREFIX, **kwargs, &blk)
26
+ def initialize(
27
+ routes:,
28
+ inflector:,
29
+ middleware_stack: Routing::Middleware::Stack.new,
30
+ prefix: ::Hanami::Router::DEFAULT_PREFIX,
31
+ **kwargs,
32
+ &blk
33
+ )
27
34
  @path_prefix = Hanami::Router::Prefix.new(prefix)
28
35
  @inflector = inflector
29
36
  @middleware_stack = middleware_stack
@@ -233,22 +240,20 @@ module Hanami
233
240
 
234
241
  def route_suffix(suffix)
235
242
  return suffix.sub(LEADING_ID_REGEX, "") if suffix && singular?
243
+
236
244
  suffix
237
245
  end
238
246
  LEADING_ID_REGEX = %r{\A/:id}
239
247
 
240
248
  def key_path_base
241
- @key_path_base ||=
242
- if @options[:to]
243
- @options[:to]
249
+ @key_path_base ||= @options[:to] || begin
250
+ if @resource_scope.any?
251
+ prefix = @resource_scope.join(CONTAINER_KEY_DELIMITER)
252
+ "#{prefix}#{CONTAINER_KEY_DELIMITER}#{@name}"
244
253
  else
245
- @name.to_s.then { |name|
246
- next name unless @resource_scope.any?
247
-
248
- prefix = @resource_scope.join(CONTAINER_KEY_DELIMITER)
249
- "#{prefix}#{CONTAINER_KEY_DELIMITER}#{name}"
250
- }
254
+ @name.to_s
251
255
  end
256
+ end
252
257
  end
253
258
 
254
259
  def route_name(action, prefix)
data/lib/hanami/slice.rb CHANGED
@@ -46,7 +46,6 @@ module Hanami
46
46
  end
47
47
  end
48
48
 
49
- # rubocop:disable Metrics/ModuleLength
50
49
  module ClassMethods
51
50
  # Returns the slice's parent.
52
51
  #
@@ -797,12 +796,24 @@ module Hanami
797
796
  #
798
797
  # @return [Array] the three-element Rack response array
799
798
  #
799
+ # @raise [Hanami::NoRoutesDefinedError] if the hanami-router gem is missing or
800
+ # no routes are defined.
801
+ #
800
802
  # @see #rack_app
801
803
  #
802
804
  # @api public
803
805
  # @since 2.0.0
804
806
  def call(...)
805
- rack_app.call(...)
807
+ if rack_app
808
+ rack_app.call(...)
809
+ else
810
+ error_message = if Hanami.bundled?("hanami-router")
811
+ "Could not handle this rack request because no routes are defined"
812
+ else
813
+ "Could not handle this rack request because the hanami router gem is missing, please add it"
814
+ end
815
+ raise NoRoutesDefinedError, error_message
816
+ end
806
817
  end
807
818
 
808
819
  private
@@ -893,10 +904,27 @@ module Hanami
893
904
  container.config.provider_dirs = [File.join("config", "providers")]
894
905
  container.config.registrations_dir = File.join("config", "registrations")
895
906
 
907
+ container.config.component_dirs.memoize = memoize_policy
908
+
896
909
  container.config.env = config.env
897
910
  container.config.inflector = config.inflector
898
911
  end
899
912
 
913
+ def memoize_policy
914
+ # Do not memoize components in the test env, so they can be stubbed if required.
915
+ return false if config.env == :test
916
+
917
+ no_memoize = config.no_memoize
918
+
919
+ if no_memoize.respond_to?(:call)
920
+ ->(component) { !no_memoize.call(component) }
921
+ elsif no_memoize.is_a?(Array) && no_memoize.any?
922
+ ->(component) { !component.key.start_with?(*no_memoize) }
923
+ else
924
+ true
925
+ end
926
+ end
927
+
900
928
  def prepare_container_component_dirs
901
929
  return unless root.directory?
902
930
 
@@ -935,6 +963,7 @@ module Hanami
935
963
  )
936
964
  end
937
965
 
966
+ # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
938
967
  def prepare_container_providers
939
968
  # Check here for the `routes` definition only, not `router` itself, because the
940
969
  # `router` requires the slice to be prepared before it can be loaded, and at this
@@ -956,19 +985,39 @@ module Hanami
956
985
 
957
986
  if register_db_provider?
958
987
  # Only register providers if the user hasn't provided their own
959
- if !container.providers[:db]
988
+ unless container.providers[:db]
960
989
  register_provider(:db, namespace: true, source: Providers::DB)
961
990
  end
962
991
 
963
- if !container.providers[:relations]
992
+ unless container.providers[:relations]
964
993
  register_provider(:relations, namespace: true, source: Providers::Relations)
965
994
  end
966
995
  end
967
996
  end
997
+
998
+ if Hanami.bundled?("i18n")
999
+ require_relative "providers/i18n"
1000
+
1001
+ if register_i18n_provider? && !container.providers[:i18n]
1002
+ register_provider(:i18n, source: Providers::I18n)
1003
+ end
1004
+ end
1005
+
1006
+ if Hanami.bundled?("hanami-mailer")
1007
+ # Explicit require here to ensure the provider source registers itself, to allow the
1008
+ # user to configure it within their own concrete provider file.
1009
+ require_relative "providers/mailers"
1010
+
1011
+ # Only register the provider if the user hasn't provided their own.
1012
+ unless container.providers[:mailers]
1013
+ register_provider(:mailers, namespace: true, source: Providers::Mailers)
1014
+ end
1015
+ end
968
1016
  end
1017
+ # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
969
1018
 
970
1019
  def prepare_autoloader
971
- autoloader.tag = "hanami.slices.#{slice_name.to_s}"
1020
+ autoloader.tag = "hanami.slices.#{slice_name}"
972
1021
 
973
1022
  # Component dirs are automatically pushed to the autoloader by dry-system's zeitwerk plugin.
974
1023
  # This method adds other dirs that are not otherwise configured as component dirs.
@@ -1009,19 +1058,20 @@ module Hanami
1009
1058
  begin
1010
1059
  require_relative "./routes"
1011
1060
  require routes_require_path
1012
- rescue LoadError => e
1013
- raise e unless e.path == routes_require_path
1061
+ rescue LoadError => exception
1062
+ raise exception unless exception.path == routes_require_path
1014
1063
  end
1015
1064
  end
1016
1065
 
1017
1066
  begin
1018
1067
  routes_class = namespace.const_get(ROUTES_CLASS_NAME)
1019
1068
  routes_class.routes
1020
- rescue NameError => e
1021
- raise e unless e.name == ROUTES_CLASS_NAME.to_sym
1069
+ rescue NameError => exception
1070
+ raise exception unless exception.name == ROUTES_CLASS_NAME.to_sym
1022
1071
  end
1023
1072
  end
1024
1073
 
1074
+ # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
1025
1075
  def load_router(inspector:)
1026
1076
  return unless routes
1027
1077
 
@@ -1065,7 +1115,7 @@ module Hanami
1065
1115
  use(Hanami::Webconsole::Middleware, config)
1066
1116
  end
1067
1117
 
1068
- if Hanami.bundled?("hanami-controller")
1118
+ if Hanami.bundled?("hanami-action")
1069
1119
  if config.actions.method_override
1070
1120
  require "rack/method_override"
1071
1121
  use(Rack::MethodOverride)
@@ -1088,6 +1138,7 @@ module Hanami
1088
1138
  middleware_stack.update(config.middleware_stack)
1089
1139
  end
1090
1140
  end
1141
+ # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
1091
1142
 
1092
1143
  def render_errors?
1093
1144
  config.render_errors
@@ -1111,6 +1162,16 @@ module Hanami
1111
1162
  source_path.join("assets").directory?
1112
1163
  end
1113
1164
 
1165
+ # Ensures an i18n provider is available in every slice.
1166
+ #
1167
+ # For the app, this will always be a standalone provider. For slices, this will be a
1168
+ # standalone provider unless the slice is configured to share the app's "i18n" component.
1169
+ def register_i18n_provider?
1170
+ return true if self == app
1171
+
1172
+ !config.shared_app_component_keys.include?("i18n")
1173
+ end
1174
+
1114
1175
  def register_db_provider?
1115
1176
  concrete_db_provider? ||
1116
1177
  db_config_dir? ||
@@ -1143,6 +1204,5 @@ module Hanami
1143
1204
 
1144
1205
  # rubocop:enable Metrics/AbcSize
1145
1206
  end
1146
- # rubocop:enable Metrics/ModuleLength
1147
1207
  end
1148
1208
  end
@@ -108,8 +108,8 @@ module Hanami
108
108
  slice_class =
109
109
  begin
110
110
  inflector.constantize("#{slice_module_name(slice_name)}#{MODULE_DELIMITER}Slice")
111
- rescue NameError => e
112
- raise e unless e.name.to_s == inflector.camelize(slice_name) || e.name.to_s == :Slice
111
+ rescue NameError => exception
112
+ raise exception unless exception.name.to_s == inflector.camelize(slice_name) || exception.name.to_s == :Slice
113
113
  end
114
114
 
115
115
  register(slice_name, slice_class)
@@ -0,0 +1,250 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Hanami
6
+ # An adapter that optionally wraps the logger configured for a Hanami app. Ensures that both
7
+ # structured and tagged logging can be used across the Hanami framework.
8
+ #
9
+ # Provides `.call` as its main entrypoint, expecting a logger object. If a compatible logger is
10
+ # given (such as the Dry Logger instance provided by default in Hanami apps), then that logger is
11
+ # returned directly and not wrapped.
12
+ #
13
+ # If a non-compatible logger is given, then it will be wrapped by an instance of UniversalLogger,
14
+ # which adapts a structured and tagged logging API to the given logger.
15
+ #
16
+ # This leads to two levels of logger enhancement:
17
+ #
18
+ # 1. Structured-capable loggers (accepts keyword arguments, but no `#tagged` method): tags are are
19
+ # provided as a `:tags` keyword argument when logging.
20
+ # 2. Legacy loggers (such as the Ruby standard `Logger`, no keyword arguments, no `#tagged`
21
+ # method): messages are logged as JSON, with tags under a `"tags"` key.
22
+ #
23
+ # This adapter is used for all loggers configured in Hanami apps.
24
+ #
25
+ # @api public
26
+ # @since x.x.x
27
+ class UniversalLogger
28
+ class << self
29
+ # Wrap a logger if needed, or return it as-is if fully compatible.
30
+ #
31
+ # @param logger [Object] the logger to wrap
32
+ # @return [Object, UniversalLogger] the original logger or wrapped logger
33
+ #
34
+ # @api private
35
+ def call(logger)
36
+ return logger if compatible_logger?(logger)
37
+
38
+ new(logger)
39
+ end
40
+
41
+ # @api private
42
+ alias_method :[], :call
43
+
44
+ # @api private
45
+ def compatible_logger?(logger)
46
+ structured_logger?(logger) && tagged_logger?(logger)
47
+ end
48
+
49
+ # @api private
50
+ def tagged_logger?(logger)
51
+ logger.respond_to?(:tagged)
52
+ end
53
+
54
+ # @api private
55
+ def structured_logger?(logger)
56
+ logger.respond_to?(:info) &&
57
+ logger.method(:info).parameters.any? { |(type, _)| type == :keyrest }
58
+ end
59
+ end
60
+
61
+ # @api public
62
+ # @since x.x.x
63
+ attr_reader :logger
64
+
65
+ # @api private
66
+ def initialize(logger)
67
+ @logger = logger
68
+ @structured_logger = self.class.structured_logger?(logger)
69
+ @tags_thread_key = :"hanami_universal_logger_tags_#{object_id}"
70
+ end
71
+
72
+ # @api private
73
+ LOG_LEVEL_METHODS = %i[debug info warn error fatal unknown].freeze
74
+ private_constant :LOG_LEVEL_METHODS
75
+
76
+ # @!method debug(message = nil, **payload, &blk)
77
+ # Logs a debug message.
78
+ #
79
+ # @param message [String, nil] the log message
80
+ # @param payload [Hash] structured data to include in the log entry
81
+ # @yieldreturn [Hash] additional payload data to merge
82
+ # @return [void]
83
+ #
84
+ # @api public
85
+ # @since x.x.x
86
+
87
+ # @!method info(message = nil, **payload, &blk)
88
+ # Logs an info message.
89
+ #
90
+ # @param message [String, nil] the log message
91
+ # @param payload [Hash] structured data to include in the log entry
92
+ # @yieldreturn [Hash] additional payload data to merge
93
+ # @return [void]
94
+ # @api public
95
+ # @since x.x.x
96
+
97
+ # @!method warn(message = nil, **payload, &blk)
98
+ # Logs a warning message.
99
+ #
100
+ # @param message [String, nil] the log message
101
+ # @param payload [Hash] structured data to include in the log entry
102
+ # @yieldreturn [Hash] additional payload data to merge
103
+ # @return [void]
104
+ #
105
+ # @api public
106
+ # @since x.x.x
107
+
108
+ # @!method error(message = nil, **payload, &blk)
109
+ # Logs an error message.
110
+ #
111
+ # @param message [String, nil] the log message
112
+ # @param payload [Hash] structured data to include in the log entry
113
+ # @yieldreturn [Hash] additional payload data to merge
114
+ # @return [void]
115
+ #
116
+ # @api public
117
+ # @since x.x.x
118
+
119
+ # @!method fatal(message = nil, **payload, &blk)
120
+ # Logs a fatal message.
121
+ #
122
+ # @param message [String, nil] the log message
123
+ # @param payload [Hash] structured data to include in the log entry
124
+ # @yieldreturn [Hash] additional payload data to merge
125
+ # @return [void]
126
+ #
127
+ # @api public
128
+ # @since x.x.x
129
+
130
+ # @!method unknown(message = nil, **payload, &blk)
131
+ # Logs a message with unknown severity.
132
+ #
133
+ # @param message [String, nil] the log message
134
+ # @param payload [Hash] structured data to include in the log entry
135
+ # @yieldreturn [Hash] additional payload data to merge
136
+ # @return [void]
137
+ #
138
+ # @api public
139
+ # @since x.x.x
140
+
141
+ LOG_LEVEL_METHODS.each do |level|
142
+ define_method(level) do |message = nil, **payload, &blk|
143
+ _log(level, message, payload, &blk)
144
+ end
145
+ end
146
+
147
+ # @api public
148
+ # @since x.x.x
149
+ def add(severity, message = nil, progname = nil, &blk)
150
+ # Convert severity to a level symbol if it's an integer (the standard Logger uses integers).
151
+ level = _severity_to_level(severity)
152
+
153
+ payload = {}
154
+ payload[:progname] = progname if progname
155
+
156
+ _log(level, message, payload, &blk)
157
+ end
158
+
159
+ # @api public
160
+ # @since x.x.x
161
+ alias_method :log, :add
162
+
163
+ # @api public
164
+ # @since x.x.x
165
+ def tagged(*tags)
166
+ previous_tags = _current_tags
167
+ self._current_tags = tags
168
+ begin
169
+ yield
170
+ ensure
171
+ self._current_tags = previous_tags
172
+ end
173
+ end
174
+
175
+ private
176
+
177
+ # Delegates any other methods to the wrapped logger.
178
+ def method_missing(method, ...)
179
+ if logger.respond_to?(method)
180
+ logger.public_send(method, ...)
181
+ else
182
+ super
183
+ end
184
+ end
185
+
186
+ def respond_to_missing?(method, include_private = false)
187
+ logger.respond_to?(method, include_private) || super
188
+ end
189
+
190
+ # Maps a standard Logger severity integer (e.g. 1) to a level name (`:info`).
191
+ #
192
+ # We need this be able to support the basic `Logger#log` and `#add` methods in addition to the
193
+ # named severity methods.
194
+ def _severity_to_level(severity)
195
+ return severity if severity.is_a?(Symbol)
196
+
197
+ SEVERITY_MAP.fetch(severity, :unknown)
198
+ end
199
+
200
+ SEVERITY_MAP = {
201
+ 0 => :debug, # Logger::DEBUG
202
+ 1 => :info, # Logger::INFO
203
+ 2 => :warn, # Logger::WARN
204
+ 3 => :error, # Logger::ERROR
205
+ 4 => :fatal, # Logger::FATAL
206
+ 5 => :unknown # Logger::UNKNOWN
207
+ }.freeze
208
+ private_constant :SEVERITY_MAP
209
+
210
+ def _log(level, message, payload, &blk)
211
+ if @structured_logger
212
+ _log_structured(level, message, payload, &blk)
213
+ else
214
+ _log_json(level, message, payload, &blk)
215
+ end
216
+ end
217
+
218
+ def _log_structured(method, message, payload)
219
+ payload = payload.merge(yield) if block_given?
220
+
221
+ tags = _current_tags
222
+ payload[:tags] = tags if tags && !tags.empty?
223
+
224
+ logger.public_send(method, message, **payload)
225
+ end
226
+
227
+ def _log_json(method, message, payload)
228
+ json_data =
229
+ if block_given?
230
+ yield
231
+ else
232
+ payload[:message] = message if message
233
+ payload
234
+ end
235
+
236
+ tags = _current_tags
237
+ json_data[:tags] = tags if tags && !tags.empty?
238
+
239
+ logger.public_send(method, JSON.generate(json_data))
240
+ end
241
+
242
+ def _current_tags
243
+ Thread.current[@tags_thread_key]
244
+ end
245
+
246
+ def _current_tags=(tags)
247
+ Thread.current[@tags_thread_key] = tags
248
+ end
249
+ end
250
+ end
@@ -7,7 +7,7 @@ module Hanami
7
7
  # @api private
8
8
  module Version
9
9
  # @api public
10
- VERSION = "2.3.2"
10
+ VERSION = "3.0.0.rc1"
11
11
 
12
12
  # @since 0.9.0
13
13
  # @api private
@@ -56,87 +56,10 @@ module Hanami
56
56
  end
57
57
  end
58
58
 
59
- # @since 2.1.0
60
- # @api private
61
- class UniversalLogger
62
- class << self
63
- # @since 2.1.0
64
- # @api private
65
- def call(logger)
66
- return logger if compatible_logger?(logger)
67
-
68
- new(logger)
69
- end
70
-
71
- # @since 2.1.0
72
- # @api private
73
- alias_method :[], :call
74
-
75
- private
76
-
77
- def compatible_logger?(logger)
78
- logger.respond_to?(:tagged) && accepts_entry_payload?(logger)
79
- end
80
-
81
- def accepts_entry_payload?(logger)
82
- logger.method(:info).parameters.any? { |(type, _)| type == :keyrest }
83
- end
84
- end
85
-
86
- # @since 2.1.0
87
- # @api private
88
- attr_reader :logger
89
-
90
- # @since 2.1.0
91
- # @api private
92
- def initialize(logger)
93
- @logger = logger
94
- end
95
-
96
- # @since 2.1.0
97
- # @api private
98
- def tagged(*, &blk)
99
- blk.call
100
- end
101
-
102
- # Logs the entry as JSON.
103
- #
104
- # This ensures a reasonable (and parseable) representation of our log payload structures for
105
- # loggers that are configured to wholly replace Hanami's default logger.
106
- #
107
- # @since 2.1.0
108
- # @api private
109
- def info(message = nil, **payload, &blk)
110
- logger.info do
111
- if blk
112
- JSON.generate(blk.call)
113
- else
114
- payload[:message] = message if message
115
- JSON.generate(payload)
116
- end
117
- end
118
- end
119
-
120
- # @see info
121
- #
122
- # @since 2.1.0
123
- # @api private
124
- def error(message = nil, **payload, &blk)
125
- logger.error do
126
- if blk
127
- JSON.generate(blk.call)
128
- else
129
- payload[:message] = message if message
130
- JSON.generate(payload)
131
- end
132
- end
133
- end
134
- end
135
-
136
59
  # @api private
137
60
  # @since 2.0.0
138
61
  def initialize(logger, env: :development)
139
- @logger = UniversalLogger[logger]
62
+ @logger = Hanami::UniversalLogger[logger]
140
63
  extend(Development) if %i[development test].include?(env)
141
64
  end
142
65
 
@@ -157,7 +80,6 @@ module Hanami
157
80
  # @since 2.0.0
158
81
  def log_request(env, status, elapsed)
159
82
  logger.tagged(:rack) do
160
-
161
83
  logger.info do
162
84
  data(env, status: status, elapsed: elapsed)
163
85
  end
@@ -189,7 +111,7 @@ module Hanami
189
111
  length: extract_content_length(env),
190
112
  params: env.fetch(ROUTER_PARAMS, EMPTY_PARAMS),
191
113
  elapsed: elapsed,
192
- elapsed_unit: MICROSECOND,
114
+ elapsed_unit: MICROSECOND
193
115
  }
194
116
  end
195
117