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.
Files changed (189) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +103 -19
  3. data/LICENSE +20 -0
  4. data/README.md +18 -35
  5. data/hanami.gemspec +36 -37
  6. data/lib/hanami/app.rb +12 -2
  7. data/lib/hanami/config/db.rb +2 -0
  8. data/lib/hanami/config/i18n.rb +138 -0
  9. data/lib/hanami/config/logger.rb +15 -7
  10. data/lib/hanami/config/null_config.rb +1 -1
  11. data/lib/hanami/config/views.rb +17 -0
  12. data/lib/hanami/config.rb +66 -22
  13. data/lib/hanami/errors.rb +6 -0
  14. data/lib/hanami/extensions/action/i18n_helper.rb +64 -0
  15. data/lib/hanami/extensions/action/name_inferrer.rb +34 -0
  16. data/lib/hanami/extensions/action/slice_configured_action.rb +31 -2
  17. data/lib/hanami/extensions/action.rb +20 -7
  18. data/lib/hanami/extensions/mailer/slice_configured_mailer.rb +134 -0
  19. data/lib/hanami/extensions/mailer.rb +28 -0
  20. data/lib/hanami/extensions/operation/slice_configured_db_operation.rb +2 -0
  21. data/lib/hanami/extensions/view/context.rb +26 -4
  22. data/lib/hanami/extensions/view/part.rb +2 -0
  23. data/lib/hanami/extensions/view/slice_configured_context.rb +7 -0
  24. data/lib/hanami/extensions/view/slice_configured_part.rb +2 -0
  25. data/lib/hanami/extensions/view/slice_configured_view.rb +8 -8
  26. data/lib/hanami/extensions/view/standard_helpers.rb +4 -0
  27. data/lib/hanami/extensions.rb +6 -1
  28. data/lib/hanami/helpers/assets_helper.rb +0 -4
  29. data/lib/hanami/helpers/form_helper.rb +1 -1
  30. data/lib/hanami/helpers/i18n_helper.rb +241 -0
  31. data/lib/hanami/logger/rack_formatter.rb +73 -0
  32. data/lib/hanami/logger/sql_formatter.rb +80 -0
  33. data/lib/hanami/logger/sql_logger.rb +48 -0
  34. data/lib/hanami/middleware/render_errors.rb +2 -2
  35. data/lib/hanami/providers/db.rb +7 -2
  36. data/lib/hanami/providers/db_logging.rb +4 -7
  37. data/lib/hanami/providers/i18n/backend.rb +373 -0
  38. data/lib/hanami/providers/i18n/locale/en.yml +57 -0
  39. data/lib/hanami/providers/i18n.rb +114 -0
  40. data/lib/hanami/providers/logger.rb +23 -1
  41. data/lib/hanami/providers/mailers.rb +101 -0
  42. data/lib/hanami/routes.rb +1 -0
  43. data/lib/hanami/settings/composite_store.rb +53 -0
  44. data/lib/hanami/settings.rb +4 -4
  45. data/lib/hanami/slice/router.rb +15 -10
  46. data/lib/hanami/slice.rb +102 -11
  47. data/lib/hanami/slice_configurable.rb +1 -3
  48. data/lib/hanami/slice_registrar.rb +2 -2
  49. data/lib/hanami/universal_logger.rb +250 -0
  50. data/lib/hanami/version.rb +1 -1
  51. data/lib/hanami/web/rack_logger.rb +2 -80
  52. data/lib/hanami/web/welcome.html.erb +443 -58
  53. data/lib/hanami.rb +4 -2
  54. metadata +34 -280
  55. data/CODE_OF_CONDUCT.md +0 -74
  56. data/FEATURES.md +0 -269
  57. data/LICENSE.md +0 -22
  58. data/spec/integration/action/cookies_spec.rb +0 -58
  59. data/spec/integration/action/csrf_protection_spec.rb +0 -54
  60. data/spec/integration/action/format_config_spec.rb +0 -129
  61. data/spec/integration/action/routes_spec.rb +0 -71
  62. data/spec/integration/action/sessions_spec.rb +0 -50
  63. data/spec/integration/action/slice_configuration_spec.rb +0 -284
  64. data/spec/integration/action/view_rendering/automatic_rendering_spec.rb +0 -247
  65. data/spec/integration/action/view_rendering/paired_view_inference_spec.rb +0 -115
  66. data/spec/integration/action/view_rendering/view_context_spec.rb +0 -221
  67. data/spec/integration/action/view_rendering_spec.rb +0 -89
  68. data/spec/integration/assets/assets_spec.rb +0 -155
  69. data/spec/integration/assets/cross_slice_assets_helpers_spec.rb +0 -129
  70. data/spec/integration/assets/serve_static_assets_spec.rb +0 -152
  71. data/spec/integration/code_loading/loading_from_app_spec.rb +0 -152
  72. data/spec/integration/code_loading/loading_from_lib_spec.rb +0 -242
  73. data/spec/integration/code_loading/loading_from_slice_spec.rb +0 -165
  74. data/spec/integration/container/application_routes_helper_spec.rb +0 -48
  75. data/spec/integration/container/auto_injection_spec.rb +0 -53
  76. data/spec/integration/container/auto_registration_spec.rb +0 -86
  77. data/spec/integration/container/autoloader_spec.rb +0 -82
  78. data/spec/integration/container/imports_spec.rb +0 -253
  79. data/spec/integration/container/prepare_container_spec.rb +0 -125
  80. data/spec/integration/container/provider_environment_spec.rb +0 -52
  81. data/spec/integration/container/provider_lifecycle_spec.rb +0 -61
  82. data/spec/integration/container/shutdown_spec.rb +0 -91
  83. data/spec/integration/container/standard_providers/rack_provider_spec.rb +0 -44
  84. data/spec/integration/container/standard_providers_spec.rb +0 -124
  85. data/spec/integration/db/auto_registration_spec.rb +0 -39
  86. data/spec/integration/db/commands_spec.rb +0 -80
  87. data/spec/integration/db/db_inflector_spec.rb +0 -57
  88. data/spec/integration/db/db_slices_spec.rb +0 -398
  89. data/spec/integration/db/db_spec.rb +0 -245
  90. data/spec/integration/db/gateways_spec.rb +0 -361
  91. data/spec/integration/db/logging_spec.rb +0 -301
  92. data/spec/integration/db/mappers_spec.rb +0 -84
  93. data/spec/integration/db/provider_config_spec.rb +0 -88
  94. data/spec/integration/db/provider_spec.rb +0 -35
  95. data/spec/integration/db/relations_spec.rb +0 -60
  96. data/spec/integration/db/repo_spec.rb +0 -300
  97. data/spec/integration/db/slices_importing_from_parent.rb +0 -130
  98. data/spec/integration/dotenv_loading_spec.rb +0 -138
  99. data/spec/integration/logging/exception_logging_spec.rb +0 -120
  100. data/spec/integration/logging/notifications_spec.rb +0 -68
  101. data/spec/integration/logging/request_logging_spec.rb +0 -202
  102. data/spec/integration/operations/extension_spec.rb +0 -122
  103. data/spec/integration/rack_app/body_parser_spec.rb +0 -108
  104. data/spec/integration/rack_app/method_override_spec.rb +0 -97
  105. data/spec/integration/rack_app/middleware_spec.rb +0 -720
  106. data/spec/integration/rack_app/non_booted_rack_app_spec.rb +0 -104
  107. data/spec/integration/rack_app/rack_app_spec.rb +0 -442
  108. data/spec/integration/rake_tasks_spec.rb +0 -107
  109. data/spec/integration/router/resource_routes_spec.rb +0 -281
  110. data/spec/integration/settings/access_in_slice_class_body_spec.rb +0 -83
  111. data/spec/integration/settings/access_to_constants_spec.rb +0 -46
  112. data/spec/integration/settings/loading_from_env_spec.rb +0 -188
  113. data/spec/integration/settings/settings_component_loading_spec.rb +0 -113
  114. data/spec/integration/settings/slice_registration_spec.rb +0 -145
  115. data/spec/integration/settings/using_types_spec.rb +0 -80
  116. data/spec/integration/setup_spec.rb +0 -165
  117. data/spec/integration/slices/external_slice_spec.rb +0 -91
  118. data/spec/integration/slices/slice_configuration_spec.rb +0 -42
  119. data/spec/integration/slices/slice_loading_spec.rb +0 -171
  120. data/spec/integration/slices/slice_registrations_spec.rb +0 -80
  121. data/spec/integration/slices/slice_routing_spec.rb +0 -219
  122. data/spec/integration/slices_spec.rb +0 -471
  123. data/spec/integration/view/config/default_context_spec.rb +0 -149
  124. data/spec/integration/view/config/inflector_spec.rb +0 -57
  125. data/spec/integration/view/config/part_class_spec.rb +0 -147
  126. data/spec/integration/view/config/part_namespace_spec.rb +0 -103
  127. data/spec/integration/view/config/paths_spec.rb +0 -119
  128. data/spec/integration/view/config/scope_class_spec.rb +0 -147
  129. data/spec/integration/view/config/scope_namespace_spec.rb +0 -103
  130. data/spec/integration/view/config/template_spec.rb +0 -38
  131. data/spec/integration/view/context/assets_spec.rb +0 -79
  132. data/spec/integration/view/context/inflector_spec.rb +0 -40
  133. data/spec/integration/view/context/request_spec.rb +0 -57
  134. data/spec/integration/view/context/routes_spec.rb +0 -84
  135. data/spec/integration/view/helpers/form_helper_spec.rb +0 -174
  136. data/spec/integration/view/helpers/part_helpers_spec.rb +0 -124
  137. data/spec/integration/view/helpers/scope_helpers_spec.rb +0 -84
  138. data/spec/integration/view/helpers/user_defined_helpers/part_helpers_spec.rb +0 -162
  139. data/spec/integration/view/helpers/user_defined_helpers/scope_helpers_spec.rb +0 -119
  140. data/spec/integration/view/parts/default_rendering_spec.rb +0 -138
  141. data/spec/integration/view/slice_configuration_spec.rb +0 -289
  142. data/spec/integration/view/views_spec.rb +0 -103
  143. data/spec/integration/web/content_security_policy_nonce_spec.rb +0 -251
  144. data/spec/integration/web/render_detailed_errors_spec.rb +0 -107
  145. data/spec/integration/web/render_errors_spec.rb +0 -242
  146. data/spec/integration/web/welcome_view_spec.rb +0 -84
  147. data/spec/spec_helper.rb +0 -28
  148. data/spec/support/app_integration.rb +0 -157
  149. data/spec/support/coverage.rb +0 -1
  150. data/spec/support/matchers.rb +0 -32
  151. data/spec/support/rspec.rb +0 -27
  152. data/spec/unit/hanami/config/actions/content_security_policy_spec.rb +0 -96
  153. data/spec/unit/hanami/config/actions/cookies_spec.rb +0 -46
  154. data/spec/unit/hanami/config/actions/csrf_protection_spec.rb +0 -58
  155. data/spec/unit/hanami/config/actions/default_values_spec.rb +0 -43
  156. data/spec/unit/hanami/config/actions/sessions_spec.rb +0 -48
  157. data/spec/unit/hanami/config/actions_spec.rb +0 -52
  158. data/spec/unit/hanami/config/base_url_spec.rb +0 -25
  159. data/spec/unit/hanami/config/console_spec.rb +0 -22
  160. data/spec/unit/hanami/config/db_spec.rb +0 -38
  161. data/spec/unit/hanami/config/inflector_spec.rb +0 -35
  162. data/spec/unit/hanami/config/logger_spec.rb +0 -195
  163. data/spec/unit/hanami/config/render_detailed_errors_spec.rb +0 -25
  164. data/spec/unit/hanami/config/render_errors_spec.rb +0 -25
  165. data/spec/unit/hanami/config/router_spec.rb +0 -44
  166. data/spec/unit/hanami/config/slices_spec.rb +0 -34
  167. data/spec/unit/hanami/config/views_spec.rb +0 -80
  168. data/spec/unit/hanami/env_spec.rb +0 -37
  169. data/spec/unit/hanami/extensions/view/context_spec.rb +0 -59
  170. data/spec/unit/hanami/helpers/assets_helper/asset_url_spec.rb +0 -120
  171. data/spec/unit/hanami/helpers/assets_helper/audio_tag_spec.rb +0 -132
  172. data/spec/unit/hanami/helpers/assets_helper/favicon_tag_spec.rb +0 -87
  173. data/spec/unit/hanami/helpers/assets_helper/image_tag_spec.rb +0 -92
  174. data/spec/unit/hanami/helpers/assets_helper/javascript_tag_spec.rb +0 -143
  175. data/spec/unit/hanami/helpers/assets_helper/stylesheet_tag_spec.rb +0 -126
  176. data/spec/unit/hanami/helpers/assets_helper/video_tag_spec.rb +0 -136
  177. data/spec/unit/hanami/helpers/form_helper_spec.rb +0 -2857
  178. data/spec/unit/hanami/port_spec.rb +0 -117
  179. data/spec/unit/hanami/providers/db/config/default_config_spec.rb +0 -100
  180. data/spec/unit/hanami/providers/db/config/gateway_spec.rb +0 -73
  181. data/spec/unit/hanami/providers/db/config_spec.rb +0 -143
  182. data/spec/unit/hanami/router/errors/not_allowed_error_spec.rb +0 -27
  183. data/spec/unit/hanami/router/errors/not_found_error_spec.rb +0 -22
  184. data/spec/unit/hanami/settings/env_store_spec.rb +0 -52
  185. data/spec/unit/hanami/settings_spec.rb +0 -111
  186. data/spec/unit/hanami/slice_configurable_spec.rb +0 -141
  187. data/spec/unit/hanami/slice_name_spec.rb +0 -47
  188. data/spec/unit/hanami/slice_spec.rb +0 -99
  189. data/spec/unit/hanami/web/rack_logger_spec.rb +0 -99
data/lib/hanami/config.rb CHANGED
@@ -6,7 +6,7 @@ require "dry/configurable"
6
6
  require "dry/inflector"
7
7
 
8
8
  require_relative "constants"
9
- require_relative "config/console"
9
+ require_relative "universal_logger"
10
10
 
11
11
  module Hanami
12
12
  # Hanami app config
@@ -105,7 +105,9 @@ module Hanami
105
105
  # @!attribute [rw] no_auto_register_paths
106
106
  # Sets the paths to skip from container auto-registration.
107
107
  #
108
- # Defaults to `["entities"]`.
108
+ # Paths are relative to `/app` or a slice root.
109
+ #
110
+ # Defaults to `["db", "entities", "relations", "structs"]`.
109
111
  #
110
112
  # @return [Array<String>] array of relative paths
111
113
  #
@@ -118,6 +120,36 @@ module Hanami
118
120
  "structs"
119
121
  ]
120
122
 
123
+ # @!attribute [rw] no_memoize
124
+ # Sets the components that should not be memoized in the container.
125
+ #
126
+ # All auto-registered components are memoized by default, meaning each component is resolved
127
+ # only once, with the same instance returned on every subsequent resolution.
128
+ #
129
+ # Use this setting to opt specific components out of memoization. It accepts an array of key
130
+ # prefixes (strings) for the simple case, or a proc for full control. The proc receives a
131
+ # `Dry::System::Component` and should return `true` for components that should _not_ be
132
+ # memoized.
133
+ #
134
+ # Individual components can also opt out by adding a `# memoize: false` magic comment at the
135
+ # top of their source file.
136
+ #
137
+ # Defaults to `[]` (all components memoized).
138
+ #
139
+ # @example Opt out by key prefix
140
+ # config.no_memoize = ["workers", "jobs"]
141
+ #
142
+ # @example Opt out with a proc
143
+ # config.no_memoize = ->(component) {
144
+ # component.key.start_with?("workers")
145
+ # }
146
+ #
147
+ # @return [Array<String>, Proc]
148
+ #
149
+ # @api public
150
+ # @since 2.3.0
151
+ setting :no_memoize, default: []
152
+
121
153
  # @!attribute [rw] base_url
122
154
  # Sets the base URL for app's web server.
123
155
  #
@@ -182,7 +214,7 @@ module Hanami
182
214
  # @since 2.1.0
183
215
  setting :render_error_responses, default: Hash.new(:internal_server_error).merge!(
184
216
  "Hanami::Router::NotAllowedError" => :not_found,
185
- "Hanami::Router::NotFoundError" => :not_found,
217
+ "Hanami::Router::NotFoundError" => :not_found
186
218
  )
187
219
 
188
220
  # @!attribute [rw] console
@@ -220,12 +252,12 @@ module Hanami
220
252
  # @since 2.0.0
221
253
  attr_reader :env
222
254
 
223
- # Returns the app's actions config, or a null config if hanami-controller is not bundled.
255
+ # Returns the app's actions config, or a null config if hanami-action is not bundled.
224
256
  #
225
- # @example When hanami-controller is bundled
257
+ # @example When hanami-action is bundled
226
258
  # config.actions.default_request_format # => :html
227
259
  #
228
- # @example When hanami-controller is not bundled
260
+ # @example When hanami-action is not bundled
229
261
  # config.actions.default_request_format # => NoMethodError
230
262
  #
231
263
  # @return [Hanami::Config::Actions, Hanami::Config::NullConfig]
@@ -248,6 +280,20 @@ module Hanami
248
280
  # @since 2.2.0
249
281
  attr_reader :db
250
282
 
283
+ # Returns the app's i18n config, or a null config if i18n is not bundled.
284
+ #
285
+ # @example When i18n is bundled
286
+ # config.i18n.default_locale # => :en
287
+ #
288
+ # @example When i18n is not bundled
289
+ # config.i18n.default_locale # => NoMethodError
290
+ #
291
+ # @return [Hanami::Config::I18n, Hanami::Config::NullConfig]
292
+ #
293
+ # @api public
294
+ # @since 2.2.0
295
+ attr_reader :i18n
296
+
251
297
  # Returns the app's middleware stack, or nil if hanami-router is not bundled.
252
298
  #
253
299
  # Use this to configure middleware that should apply to all routes.
@@ -320,7 +366,7 @@ module Hanami
320
366
  self.render_detailed_errors = (env == :development)
321
367
  load_from_env
322
368
 
323
- @actions = load_dependent_config("hanami-controller") {
369
+ @actions = load_dependent_config("hanami-action") {
324
370
  require_relative "config/actions"
325
371
  Actions.new
326
372
  }
@@ -332,6 +378,11 @@ module Hanami
332
378
 
333
379
  @db = load_dependent_config("hanami-db") { DB.new }
334
380
 
381
+ @i18n = load_dependent_config("i18n") {
382
+ require_relative "config/i18n"
383
+ I18n.new
384
+ }
385
+
335
386
  @logger = Config::Logger.new(env: env, app_name: app_name)
336
387
 
337
388
  @middleware = load_dependent_config("hanami-router") {
@@ -362,6 +413,7 @@ module Hanami
362
413
  @actions = source.actions.dup
363
414
  @assets = source.assets.dup
364
415
  @db = source.db.dup
416
+ @i18n = source.i18n.dup
365
417
  @logger = source.logger.dup
366
418
  @middleware = source.middleware.dup
367
419
  @router = source.router.dup.tap do |router|
@@ -385,8 +437,6 @@ module Hanami
385
437
  logger.finalize!
386
438
  router.finalize!
387
439
 
388
- use_body_parser_middleware
389
-
390
440
  super
391
441
  end
392
442
 
@@ -434,13 +484,19 @@ module Hanami
434
484
  # Sets the app's logger instance.
435
485
  #
436
486
  # This entirely replaces the default `Dry::Logger::Dispatcher` instance that would have been
487
+ # configured via {#logger}.
488
+ #
489
+ # The provided logger will be wrapped with {Hanami::UniversalLogger} if it doesn't support both
490
+ # keyword arguments and `#tagged` logging, ensuring compatibility with Hanami's logging
491
+ # interface.
437
492
  #
438
493
  # @see #logger_instance
494
+ # @see Hanami::UniversalLogger
439
495
  #
440
496
  # @api public
441
497
  # @since 2.0.0
442
498
  def logger=(logger_instance)
443
- @logger_instance = logger_instance
499
+ @logger_instance = Hanami::UniversalLogger[logger_instance]
444
500
  end
445
501
 
446
502
  # rubocop:enable Style/TrivialAccessors
@@ -484,18 +540,6 @@ module Hanami
484
540
  self.slices = ENV["HANAMI_SLICES"]&.split(",")&.map(&:strip)
485
541
  end
486
542
 
487
- DEFAULT_MIDDLEWARE_PARSERS = {
488
- form: ["multipart/form-data"],
489
- json: ["application/json", "application/vnd.api+json"]
490
- }.freeze
491
- private_constant :DEFAULT_MIDDLEWARE_PARSERS
492
-
493
- def use_body_parser_middleware
494
- return unless Hanami.bundled?("hanami-router") && Hanami.bundled?("hanami-controller")
495
-
496
- middleware.use(:body_parser, [DEFAULT_MIDDLEWARE_PARSERS])
497
- end
498
-
499
543
  def load_dependent_config(gem_name)
500
544
  if Hanami.bundled?(gem_name)
501
545
  yield
data/lib/hanami/errors.rb CHANGED
@@ -25,6 +25,12 @@ module Hanami
25
25
  # @since 2.0.0
26
26
  ComponentLoadError = Class.new(Error)
27
27
 
28
+ # Error raised when there are no routes defined.
29
+ #
30
+ # @api public
31
+ # @since 2.4.0
32
+ NoRoutesDefinedError = Class.new(Error)
33
+
28
34
  # Error raised when unsupported middleware configuration is given.
29
35
  #
30
36
  # @see Hanami::Slice::Routing::Middleware::Stack#use
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hanami
4
+ module Extensions
5
+ module Action
6
+ # Action translation and localization helpers.
7
+ #
8
+ # These helpers are automatically available on `Hanami::Action` when the `i18n` gem is
9
+ # bundled.
10
+ #
11
+ # When relative translation keys (with a leading dot) are given, they are expanded against a
12
+ # the action's name. For example, `t(".not_found")` within `Main::Actions::Posts::Show`
13
+ # becomes `"posts.show.not_found"`.
14
+ #
15
+ # @example Basic translation in an action
16
+ # module Main
17
+ # module Actions
18
+ # module Posts
19
+ # class Create < Main::Action
20
+ # def handle(req, res)
21
+ # res.flash[:notice] = t("messages.post_created")
22
+ # res.redirect_to routes.path(:posts)
23
+ # end
24
+ # end
25
+ # end
26
+ # end
27
+ # end
28
+ #
29
+ # @example Relative key lookup
30
+ # # In Main::Actions::Posts::Show, this looks up "posts.show.not_found"
31
+ # t(".not_found")
32
+ #
33
+ # @see Hanami::Helpers::I18nHelper::Methods
34
+ #
35
+ # @api public
36
+ # @since 3.0.0
37
+ module I18nHelper
38
+ include Hanami::Helpers::I18nHelper::Methods
39
+
40
+ private
41
+
42
+ def _i18n
43
+ i18n
44
+ end
45
+
46
+ def _resolve_i18n_key(key)
47
+ return key unless key.to_s.start_with?(".")
48
+
49
+ key_base = self.class.config.i18n_key_base
50
+
51
+ unless key_base
52
+ raise(
53
+ ::I18n::ArgumentError,
54
+ "Cannot use relative translation key #{key.inspect} outside of a slice-configured action. " \
55
+ "Use an absolute key (without a leading dot) instead."
56
+ )
57
+ end
58
+
59
+ "#{key_base}#{key}"
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hanami
4
+ module Extensions
5
+ module Action
6
+ # Infers an action's name (e.g. `posts.show`) from its class name relative to its slice
7
+ # namespace.
8
+ #
9
+ # @api private
10
+ class NameInferrer
11
+ class << self
12
+ # @example
13
+ # NameInferrer.call(action_class_name: "Main::Actions::Posts::Show", slice: Main::Slice)
14
+ # # => "posts.show"
15
+ #
16
+ # @param action_class_name [String, nil] the action class name
17
+ # @param slice [Hanami::Slice] the slice the action belongs to
18
+ #
19
+ # @return [String, nil] the inferred name, or nil if `action_class_name` is nil
20
+ def call(action_class_name:, slice:)
21
+ return nil unless action_class_name
22
+
23
+ slice
24
+ .inflector
25
+ .underscore(action_class_name)
26
+ .sub(%r{^#{slice.slice_name.path}#{PATH_DELIMITER}}, "")
27
+ .sub(%r{^#{slice.config.actions.name_inference_base}#{PATH_DELIMITER}}, "")
28
+ .gsub("/", CONTAINER_KEY_DELIMITER)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -3,11 +3,12 @@
3
3
  module Hanami
4
4
  module Extensions
5
5
  module Action
6
+ require_relative "name_inferrer"
7
+
6
8
  # Provides slice-specific configuration and behavior for any action class defined
7
9
  # within a slice's module namespace.
8
10
  #
9
11
  # @api private
10
- # @since 2.0.0
11
12
  class SliceConfiguredAction < Module
12
13
  attr_reader :slice
13
14
 
@@ -18,7 +19,9 @@ module Hanami
18
19
 
19
20
  def extended(action_class)
20
21
  configure_action(action_class)
22
+ configure_action_i18n(action_class)
21
23
  extend_behavior(action_class)
24
+ define_inherited
22
25
  define_new
23
26
  end
24
27
 
@@ -34,6 +37,7 @@ module Hanami
34
37
  view_context_class = method(:view_context_class)
35
38
  resolve_routes = method(:resolve_routes)
36
39
  resolve_rack_monitor = method(:resolve_rack_monitor)
40
+ resolve_i18n = method(:resolve_i18n)
37
41
 
38
42
  define_method(:new) do |**kwargs|
39
43
  super(
@@ -41,11 +45,24 @@ module Hanami
41
45
  view_context_class: kwargs.fetch(:view_context_class) { view_context_class.() },
42
46
  routes: kwargs.fetch(:routes) { resolve_routes.() },
43
47
  rack_monitor: kwargs.fetch(:rack_monitor) { resolve_rack_monitor.() },
48
+ i18n: kwargs.fetch(:i18n) { resolve_i18n.() },
44
49
  **kwargs,
45
50
  )
46
51
  end
47
52
  end
48
53
 
54
+ # Defines an `inherited` hook on this module so each subclass of the slice-configured
55
+ # action gets its own `config.i18n_key_base`, inferred from its class name. Mirrors the
56
+ # approach used by `SliceConfiguredView` for `config.template`.
57
+ def define_inherited
58
+ configure_action_i18n = method(:configure_action_i18n)
59
+
60
+ define_method(:inherited) do |subclass|
61
+ super(subclass)
62
+ configure_action_i18n.call(subclass)
63
+ end
64
+ end
65
+
49
66
  def configure_action(action_class)
50
67
  action_class.settings.each do |setting|
51
68
  # Configure the action from config on the slice, _unless it has already been configured
@@ -98,6 +115,14 @@ module Hanami
98
115
  end
99
116
  end
100
117
 
118
+ def configure_action_i18n(action_class)
119
+ return unless action_class.config.respond_to?(:i18n_key_base=)
120
+
121
+ action_class.config.i18n_key_base = NameInferrer.call(
122
+ action_class_name: action_class.name, slice:
123
+ )
124
+ end
125
+
101
126
  def extend_behavior(action_class)
102
127
  if actions_config.sessions.enabled?
103
128
  require "hanami/action/session"
@@ -118,7 +143,7 @@ module Hanami
118
143
  def resolve_paired_view(action_class)
119
144
  view_identifiers = actions_config.view_name_inferrer.call(
120
145
  action_class_name: action_class.name,
121
- slice: slice,
146
+ slice: slice
122
147
  )
123
148
 
124
149
  view_identifiers.each do |identifier|
@@ -152,6 +177,10 @@ module Hanami
152
177
  slice.app["rack.monitor"] if slice.app.key?("rack.monitor")
153
178
  end
154
179
 
180
+ def resolve_i18n
181
+ slice["i18n"] if slice.key?("i18n")
182
+ end
183
+
155
184
  def actions_config
156
185
  slice.config.actions
157
186
  end
@@ -9,7 +9,7 @@ module Hanami
9
9
  # Integrated behavior for `Hanami::Action` classes within Hanami apps.
10
10
  #
11
11
  # @see InstanceMethods
12
- # @see https://github.com/hanami/controller
12
+ # @see https://github.com/hanami/hanami-action
13
13
  #
14
14
  # @api public
15
15
  # @since 2.0.0
@@ -43,8 +43,7 @@ module Hanami
43
43
  # @api private
44
44
  attr_reader :view_context_class
45
45
 
46
- # Returns the app or slice's {Hanami::Slice::RoutesHelper RoutesHelper} for use within
47
- # action instance methods.
46
+ # Returns the slice's {Hanami::Slice::RoutesHelper RoutesHelper}.
48
47
  #
49
48
  # @return [Hanami::Slice::RoutesHelper]
50
49
  #
@@ -52,8 +51,7 @@ module Hanami
52
51
  # @since 2.0.0
53
52
  attr_reader :routes
54
53
 
55
- # Returns the app or slice's `Dry::Monitor::Rack::Middleware` for use within
56
- # action instance methods.
54
+ # Returns the slice's `Dry::Monitor::Rack::Middleware`.
57
55
  #
58
56
  # @return [Dry::Monitor::Rack::Middleware]
59
57
  #
@@ -61,6 +59,14 @@ module Hanami
61
59
  # @since 2.0.0
62
60
  attr_reader :rack_monitor
63
61
 
62
+ # Returns the slice's i18n backend.
63
+ #
64
+ # @return [Hanami::Providers::I18n::Backend]
65
+ #
66
+ # @api public
67
+ # @since x.x.x
68
+ attr_reader :i18n
69
+
64
70
  # @overload def initialize(routes: nil, **kwargs)
65
71
  # Returns a new `Hanami::Action` with app components injected as dependencies.
66
72
  #
@@ -71,11 +77,12 @@ module Hanami
71
77
  #
72
78
  # @api public
73
79
  # @since 2.0.0
74
- def initialize(view: nil, view_context_class: nil, rack_monitor: nil, routes: nil, **kwargs)
80
+ def initialize(view: nil, view_context_class: nil, rack_monitor: nil, routes: nil, i18n: nil, **kwargs)
75
81
  @view = view
76
82
  @view_context_class = view_context_class
77
83
  @routes = routes
78
84
  @rack_monitor = rack_monitor
85
+ @i18n = i18n
79
86
 
80
87
  super(**kwargs)
81
88
  end
@@ -109,7 +116,7 @@ module Hanami
109
116
  end
110
117
 
111
118
  # @api private
112
- def view_context_options(request, response) # rubocop:disable Lint:UnusedMethodArgument
119
+ def view_context_options(request, response)
113
120
  {request: request}
114
121
  end
115
122
 
@@ -132,3 +139,9 @@ module Hanami
132
139
  end
133
140
 
134
141
  Hanami::Action.include(Hanami::Extensions::Action)
142
+
143
+ if Hanami.bundled?("i18n")
144
+ require_relative "action/i18n_helper"
145
+ Hanami::Action.setting(:i18n_key_base)
146
+ Hanami::Action.include(Hanami::Extensions::Action::I18nHelper)
147
+ end
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hanami
4
+ module Extensions
5
+ module Mailer
6
+ # Provides slice-specific configuration and behavior for any mailer class defined within a
7
+ # slice's module namespace.
8
+ #
9
+ # This injects the slice's `"mailers.delivery_method"` component into mailer instances, and
10
+ # points the mailer's view at a slice-configured view class so that mailer templates behave
11
+ # like regular Hanami view templates — sharing the slice's view context, parts, scopes and
12
+ # helpers (including i18n). The only behavior not available to mailer views is
13
+ # request-related state (`request`/`session`/`flash`/`csrf_token`), since mailers are not
14
+ # rendered from a request.
15
+ #
16
+ # @api private
17
+ class SliceConfiguredMailer < Module
18
+ attr_reader :slice
19
+
20
+ def initialize(slice)
21
+ super()
22
+ @slice = slice
23
+ end
24
+
25
+ def extended(mailer_class)
26
+ configure_mailer(mailer_class)
27
+ define_new
28
+ define_inherited
29
+ end
30
+
31
+ def inspect
32
+ "#<#{self.class.name}[#{slice.name}]>"
33
+ end
34
+
35
+ private
36
+
37
+ def define_new
38
+ resolve_delivery_method = method(:resolve_delivery_method)
39
+
40
+ define_method(:new) do |**kwargs|
41
+ super(
42
+ delivery_method: kwargs.fetch(:delivery_method) { resolve_delivery_method.() },
43
+ **kwargs
44
+ )
45
+ end
46
+ end
47
+
48
+ # Reconfigures the template name for each subclass.
49
+ def define_inherited
50
+ template_name = method(:template_name)
51
+
52
+ define_method(:inherited) do |subclass|
53
+ super(subclass)
54
+
55
+ if (template = template_name.(subclass))
56
+ subclass.config.template = template
57
+ end
58
+ end
59
+ end
60
+
61
+ def resolve_delivery_method
62
+ slice["mailers.delivery_method"] if slice.key?("mailers.delivery_method")
63
+ end
64
+
65
+ def configure_mailer(mailer_class)
66
+ return unless Hanami.bundled?("hanami-view")
67
+
68
+ # Build the mailer's view from a slice-configured view class, so it inherits the slice's
69
+ # context, parts, scopes, paths and helpers. The mailer only needs to supply its own
70
+ # template name.
71
+ mailer_class.config.view_class = mailer_view_class
72
+
73
+ if (template = template_name(mailer_class))
74
+ mailer_class.config.template = template
75
+ end
76
+ end
77
+
78
+ # Returns the view class that mailer views are built from, defining a `Mailers::View` within
79
+ # the slice if one is not already present.
80
+ #
81
+ # This mirrors how the view extension defines a `Views::Context`: because the class is
82
+ # defined within the slice's namespace, it is configured automatically (paths, context,
83
+ # parts, scopes, helpers) just like any other view in the slice. A user may define their own
84
+ # `<Slice>::Mailers::View` to customize mailer rendering; it is used when present.
85
+ def mailer_view_class
86
+ namespace = mailers_namespace
87
+
88
+ if namespace.const_defined?(:View, _inherit = false)
89
+ namespace.const_get(:View)
90
+ else
91
+ namespace.const_set(:View, define_mailer_view)
92
+ end
93
+ end
94
+
95
+ # Defines and configures a view class for the slice's mailers, inheriting from the slice's
96
+ # own base view class if present, otherwise from the plain `Hanami::View`.
97
+ def define_mailer_view
98
+ superclass =
99
+ begin
100
+ slice.inflector.constantize("#{slice.namespace.name}::View")
101
+ rescue NameError
102
+ Hanami::View
103
+ end
104
+
105
+ Class.new(superclass).tap { |klass|
106
+ # Call configure_for_slice explicitly, since this is an anonymous class at this point,
107
+ # so the slice cannot be inferred from its name.
108
+ klass.configure_for_slice(slice)
109
+ }
110
+ end
111
+
112
+ def mailers_namespace
113
+ if slice.namespace.const_defined?(:Mailers, _inherit = false)
114
+ slice.namespace.const_get(:Mailers)
115
+ else
116
+ slice.namespace.const_set(:Mailers, Module.new)
117
+ end
118
+ end
119
+
120
+ # Returns the template name for the mailer, retaining the `mailers/` segment from the
121
+ # mailer's namespace so templates resolve under `templates/mailers/` within the slice.
122
+ #
123
+ # For example, `App::Mailers::Welcome` will use template `"mailers/welcome"`.
124
+ def template_name(mailer_class)
125
+ return unless mailer_class.name
126
+
127
+ slice.inflector
128
+ .underscore(mailer_class.name)
129
+ .sub(/^#{slice.slice_name.path}\//, "")
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "hanami/mailer"
4
+ require_relative "mailer/slice_configured_mailer"
5
+
6
+ module Hanami
7
+ module Extensions
8
+ # Integrated behavior for `Hanami::Mailer` classes within Hanami apps.
9
+ #
10
+ # @api private
11
+ module Mailer
12
+ def self.included(mailer_class)
13
+ super
14
+
15
+ mailer_class.extend(Hanami::SliceConfigurable)
16
+ mailer_class.extend(ClassMethods)
17
+ end
18
+
19
+ module ClassMethods
20
+ def configure_for_slice(slice)
21
+ extend SliceConfiguredMailer.new(slice)
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+
28
+ Hanami::Mailer.include(Hanami::Extensions::Mailer)
@@ -51,6 +51,7 @@ module Hanami
51
51
  attr_reader :rom
52
52
 
53
53
  def initialize(rom:, **)
54
+ super()
54
55
  @rom = rom
55
56
  end
56
57
  end
@@ -59,6 +60,7 @@ module Hanami
59
60
  attr_reader :slice
60
61
 
61
62
  def initialize(slice)
63
+ super()
62
64
  @slice = slice
63
65
  end
64
66