hanami 2.3.2 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +103 -19
- data/LICENSE +20 -0
- data/README.md +18 -35
- data/hanami.gemspec +36 -37
- data/lib/hanami/app.rb +12 -2
- data/lib/hanami/config/db.rb +2 -0
- data/lib/hanami/config/i18n.rb +138 -0
- data/lib/hanami/config/logger.rb +15 -7
- data/lib/hanami/config/null_config.rb +1 -1
- data/lib/hanami/config/views.rb +17 -0
- data/lib/hanami/config.rb +66 -22
- data/lib/hanami/errors.rb +6 -0
- data/lib/hanami/extensions/action/i18n_helper.rb +64 -0
- data/lib/hanami/extensions/action/name_inferrer.rb +34 -0
- data/lib/hanami/extensions/action/slice_configured_action.rb +31 -2
- data/lib/hanami/extensions/action.rb +20 -7
- data/lib/hanami/extensions/mailer/slice_configured_mailer.rb +134 -0
- data/lib/hanami/extensions/mailer.rb +28 -0
- data/lib/hanami/extensions/operation/slice_configured_db_operation.rb +2 -0
- data/lib/hanami/extensions/view/context.rb +26 -4
- data/lib/hanami/extensions/view/part.rb +2 -0
- data/lib/hanami/extensions/view/slice_configured_context.rb +7 -0
- data/lib/hanami/extensions/view/slice_configured_part.rb +2 -0
- data/lib/hanami/extensions/view/slice_configured_view.rb +8 -8
- data/lib/hanami/extensions/view/standard_helpers.rb +4 -0
- data/lib/hanami/extensions.rb +6 -1
- data/lib/hanami/helpers/assets_helper.rb +0 -4
- data/lib/hanami/helpers/form_helper.rb +1 -1
- data/lib/hanami/helpers/i18n_helper.rb +241 -0
- data/lib/hanami/logger/rack_formatter.rb +73 -0
- data/lib/hanami/logger/sql_formatter.rb +80 -0
- data/lib/hanami/logger/sql_logger.rb +48 -0
- data/lib/hanami/middleware/render_errors.rb +2 -2
- data/lib/hanami/providers/db.rb +7 -2
- data/lib/hanami/providers/db_logging.rb +4 -7
- data/lib/hanami/providers/i18n/backend.rb +373 -0
- data/lib/hanami/providers/i18n/locale/en.yml +57 -0
- data/lib/hanami/providers/i18n.rb +114 -0
- data/lib/hanami/providers/logger.rb +23 -1
- data/lib/hanami/providers/mailers.rb +101 -0
- data/lib/hanami/routes.rb +1 -0
- data/lib/hanami/settings/composite_store.rb +53 -0
- data/lib/hanami/settings.rb +4 -4
- data/lib/hanami/slice/router.rb +15 -10
- data/lib/hanami/slice.rb +102 -11
- data/lib/hanami/slice_configurable.rb +1 -3
- data/lib/hanami/slice_registrar.rb +2 -2
- data/lib/hanami/universal_logger.rb +250 -0
- data/lib/hanami/version.rb +1 -1
- data/lib/hanami/web/rack_logger.rb +2 -80
- data/lib/hanami/web/welcome.html.erb +443 -58
- data/lib/hanami.rb +4 -2
- metadata +34 -280
- data/CODE_OF_CONDUCT.md +0 -74
- data/FEATURES.md +0 -269
- data/LICENSE.md +0 -22
- data/spec/integration/action/cookies_spec.rb +0 -58
- data/spec/integration/action/csrf_protection_spec.rb +0 -54
- data/spec/integration/action/format_config_spec.rb +0 -129
- data/spec/integration/action/routes_spec.rb +0 -71
- data/spec/integration/action/sessions_spec.rb +0 -50
- data/spec/integration/action/slice_configuration_spec.rb +0 -284
- data/spec/integration/action/view_rendering/automatic_rendering_spec.rb +0 -247
- data/spec/integration/action/view_rendering/paired_view_inference_spec.rb +0 -115
- data/spec/integration/action/view_rendering/view_context_spec.rb +0 -221
- data/spec/integration/action/view_rendering_spec.rb +0 -89
- data/spec/integration/assets/assets_spec.rb +0 -155
- data/spec/integration/assets/cross_slice_assets_helpers_spec.rb +0 -129
- data/spec/integration/assets/serve_static_assets_spec.rb +0 -152
- data/spec/integration/code_loading/loading_from_app_spec.rb +0 -152
- data/spec/integration/code_loading/loading_from_lib_spec.rb +0 -242
- data/spec/integration/code_loading/loading_from_slice_spec.rb +0 -165
- data/spec/integration/container/application_routes_helper_spec.rb +0 -48
- data/spec/integration/container/auto_injection_spec.rb +0 -53
- data/spec/integration/container/auto_registration_spec.rb +0 -86
- data/spec/integration/container/autoloader_spec.rb +0 -82
- data/spec/integration/container/imports_spec.rb +0 -253
- data/spec/integration/container/prepare_container_spec.rb +0 -125
- data/spec/integration/container/provider_environment_spec.rb +0 -52
- data/spec/integration/container/provider_lifecycle_spec.rb +0 -61
- data/spec/integration/container/shutdown_spec.rb +0 -91
- data/spec/integration/container/standard_providers/rack_provider_spec.rb +0 -44
- data/spec/integration/container/standard_providers_spec.rb +0 -124
- data/spec/integration/db/auto_registration_spec.rb +0 -39
- data/spec/integration/db/commands_spec.rb +0 -80
- data/spec/integration/db/db_inflector_spec.rb +0 -57
- data/spec/integration/db/db_slices_spec.rb +0 -398
- data/spec/integration/db/db_spec.rb +0 -245
- data/spec/integration/db/gateways_spec.rb +0 -361
- data/spec/integration/db/logging_spec.rb +0 -301
- data/spec/integration/db/mappers_spec.rb +0 -84
- data/spec/integration/db/provider_config_spec.rb +0 -88
- data/spec/integration/db/provider_spec.rb +0 -35
- data/spec/integration/db/relations_spec.rb +0 -60
- data/spec/integration/db/repo_spec.rb +0 -300
- data/spec/integration/db/slices_importing_from_parent.rb +0 -130
- data/spec/integration/dotenv_loading_spec.rb +0 -138
- data/spec/integration/logging/exception_logging_spec.rb +0 -120
- data/spec/integration/logging/notifications_spec.rb +0 -68
- data/spec/integration/logging/request_logging_spec.rb +0 -202
- data/spec/integration/operations/extension_spec.rb +0 -122
- data/spec/integration/rack_app/body_parser_spec.rb +0 -108
- data/spec/integration/rack_app/method_override_spec.rb +0 -97
- data/spec/integration/rack_app/middleware_spec.rb +0 -720
- data/spec/integration/rack_app/non_booted_rack_app_spec.rb +0 -104
- data/spec/integration/rack_app/rack_app_spec.rb +0 -442
- data/spec/integration/rake_tasks_spec.rb +0 -107
- data/spec/integration/router/resource_routes_spec.rb +0 -281
- data/spec/integration/settings/access_in_slice_class_body_spec.rb +0 -83
- data/spec/integration/settings/access_to_constants_spec.rb +0 -46
- data/spec/integration/settings/loading_from_env_spec.rb +0 -188
- data/spec/integration/settings/settings_component_loading_spec.rb +0 -113
- data/spec/integration/settings/slice_registration_spec.rb +0 -145
- data/spec/integration/settings/using_types_spec.rb +0 -80
- data/spec/integration/setup_spec.rb +0 -165
- data/spec/integration/slices/external_slice_spec.rb +0 -91
- data/spec/integration/slices/slice_configuration_spec.rb +0 -42
- data/spec/integration/slices/slice_loading_spec.rb +0 -171
- data/spec/integration/slices/slice_registrations_spec.rb +0 -80
- data/spec/integration/slices/slice_routing_spec.rb +0 -219
- data/spec/integration/slices_spec.rb +0 -471
- data/spec/integration/view/config/default_context_spec.rb +0 -149
- data/spec/integration/view/config/inflector_spec.rb +0 -57
- data/spec/integration/view/config/part_class_spec.rb +0 -147
- data/spec/integration/view/config/part_namespace_spec.rb +0 -103
- data/spec/integration/view/config/paths_spec.rb +0 -119
- data/spec/integration/view/config/scope_class_spec.rb +0 -147
- data/spec/integration/view/config/scope_namespace_spec.rb +0 -103
- data/spec/integration/view/config/template_spec.rb +0 -38
- data/spec/integration/view/context/assets_spec.rb +0 -79
- data/spec/integration/view/context/inflector_spec.rb +0 -40
- data/spec/integration/view/context/request_spec.rb +0 -57
- data/spec/integration/view/context/routes_spec.rb +0 -84
- data/spec/integration/view/helpers/form_helper_spec.rb +0 -174
- data/spec/integration/view/helpers/part_helpers_spec.rb +0 -124
- data/spec/integration/view/helpers/scope_helpers_spec.rb +0 -84
- data/spec/integration/view/helpers/user_defined_helpers/part_helpers_spec.rb +0 -162
- data/spec/integration/view/helpers/user_defined_helpers/scope_helpers_spec.rb +0 -119
- data/spec/integration/view/parts/default_rendering_spec.rb +0 -138
- data/spec/integration/view/slice_configuration_spec.rb +0 -289
- data/spec/integration/view/views_spec.rb +0 -103
- data/spec/integration/web/content_security_policy_nonce_spec.rb +0 -251
- data/spec/integration/web/render_detailed_errors_spec.rb +0 -107
- data/spec/integration/web/render_errors_spec.rb +0 -242
- data/spec/integration/web/welcome_view_spec.rb +0 -84
- data/spec/spec_helper.rb +0 -28
- data/spec/support/app_integration.rb +0 -157
- data/spec/support/coverage.rb +0 -1
- data/spec/support/matchers.rb +0 -32
- data/spec/support/rspec.rb +0 -27
- data/spec/unit/hanami/config/actions/content_security_policy_spec.rb +0 -96
- data/spec/unit/hanami/config/actions/cookies_spec.rb +0 -46
- data/spec/unit/hanami/config/actions/csrf_protection_spec.rb +0 -58
- data/spec/unit/hanami/config/actions/default_values_spec.rb +0 -43
- data/spec/unit/hanami/config/actions/sessions_spec.rb +0 -48
- data/spec/unit/hanami/config/actions_spec.rb +0 -52
- data/spec/unit/hanami/config/base_url_spec.rb +0 -25
- data/spec/unit/hanami/config/console_spec.rb +0 -22
- data/spec/unit/hanami/config/db_spec.rb +0 -38
- data/spec/unit/hanami/config/inflector_spec.rb +0 -35
- data/spec/unit/hanami/config/logger_spec.rb +0 -195
- data/spec/unit/hanami/config/render_detailed_errors_spec.rb +0 -25
- data/spec/unit/hanami/config/render_errors_spec.rb +0 -25
- data/spec/unit/hanami/config/router_spec.rb +0 -44
- data/spec/unit/hanami/config/slices_spec.rb +0 -34
- data/spec/unit/hanami/config/views_spec.rb +0 -80
- data/spec/unit/hanami/env_spec.rb +0 -37
- data/spec/unit/hanami/extensions/view/context_spec.rb +0 -59
- data/spec/unit/hanami/helpers/assets_helper/asset_url_spec.rb +0 -120
- data/spec/unit/hanami/helpers/assets_helper/audio_tag_spec.rb +0 -132
- data/spec/unit/hanami/helpers/assets_helper/favicon_tag_spec.rb +0 -87
- data/spec/unit/hanami/helpers/assets_helper/image_tag_spec.rb +0 -92
- data/spec/unit/hanami/helpers/assets_helper/javascript_tag_spec.rb +0 -143
- data/spec/unit/hanami/helpers/assets_helper/stylesheet_tag_spec.rb +0 -126
- data/spec/unit/hanami/helpers/assets_helper/video_tag_spec.rb +0 -136
- data/spec/unit/hanami/helpers/form_helper_spec.rb +0 -2857
- data/spec/unit/hanami/port_spec.rb +0 -117
- data/spec/unit/hanami/providers/db/config/default_config_spec.rb +0 -100
- data/spec/unit/hanami/providers/db/config/gateway_spec.rb +0 -73
- data/spec/unit/hanami/providers/db/config_spec.rb +0 -143
- data/spec/unit/hanami/router/errors/not_allowed_error_spec.rb +0 -27
- data/spec/unit/hanami/router/errors/not_found_error_spec.rb +0 -22
- data/spec/unit/hanami/settings/env_store_spec.rb +0 -52
- data/spec/unit/hanami/settings_spec.rb +0 -111
- data/spec/unit/hanami/slice_configurable_spec.rb +0 -141
- data/spec/unit/hanami/slice_name_spec.rb +0 -47
- data/spec/unit/hanami/slice_spec.rb +0 -99
- data/spec/unit/hanami/web/rack_logger_spec.rb +0 -99
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 "
|
|
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
|
-
#
|
|
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-
|
|
255
|
+
# Returns the app's actions config, or a null config if hanami-action is not bundled.
|
|
224
256
|
#
|
|
225
|
-
# @example When hanami-
|
|
257
|
+
# @example When hanami-action is bundled
|
|
226
258
|
# config.actions.default_request_format # => :html
|
|
227
259
|
#
|
|
228
|
-
# @example When hanami-
|
|
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-
|
|
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/
|
|
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
|
|
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
|
|
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)
|
|
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)
|