hanami 2.1.0.beta2 → 2.1.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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +41 -9
  3. data/hanami.gemspec +1 -1
  4. data/lib/hanami/config/actions.rb +14 -0
  5. data/lib/hanami/extensions/action/slice_configured_action.rb +5 -5
  6. data/lib/hanami/extensions/action.rb +4 -4
  7. data/lib/hanami/extensions/view/context.rb +0 -11
  8. data/lib/hanami/extensions/view/part.rb +51 -2
  9. data/lib/hanami/extensions/view/slice_configured_part.rb +72 -0
  10. data/lib/hanami/extensions/view/slice_configured_view.rb +2 -2
  11. data/lib/hanami/helpers/assets_helper.rb +31 -43
  12. data/lib/hanami/routes.rb +33 -2
  13. data/lib/hanami/slice.rb +12 -2
  14. data/lib/hanami/slice_registrar.rb +48 -23
  15. data/lib/hanami/version.rb +1 -1
  16. data/lib/hanami/web/rack_logger.rb +70 -2
  17. data/lib/hanami/web/welcome.html.erb +203 -0
  18. data/lib/hanami/web/welcome.rb +46 -0
  19. data/spec/integration/assets/assets_spec.rb +14 -3
  20. data/spec/integration/logging/request_logging_spec.rb +65 -7
  21. data/spec/integration/rack_app/method_override_spec.rb +97 -0
  22. data/spec/integration/slices_spec.rb +275 -5
  23. data/spec/integration/view/context/assets_spec.rb +0 -8
  24. data/spec/integration/view/context/inflector_spec.rb +0 -8
  25. data/spec/integration/view/context/settings_spec.rb +0 -8
  26. data/spec/integration/view/helpers/part_helpers_spec.rb +2 -2
  27. data/spec/integration/view/helpers/user_defined_helpers/part_helpers_spec.rb +10 -10
  28. data/spec/integration/view/parts/default_rendering_spec.rb +138 -0
  29. data/spec/integration/web/welcome_view_spec.rb +84 -0
  30. data/spec/support/app_integration.rb +22 -4
  31. data/spec/unit/hanami/helpers/assets_helper/audio_tag_spec.rb +2 -2
  32. data/spec/unit/hanami/helpers/assets_helper/{favicon_link_tag_spec.rb → favicon_tag_spec.rb} +8 -12
  33. data/spec/unit/hanami/helpers/assets_helper/image_tag_spec.rb +1 -1
  34. data/spec/unit/hanami/helpers/assets_helper/javascript_tag_spec.rb +3 -3
  35. data/spec/unit/hanami/helpers/assets_helper/{stylesheet_link_tag_spec.rb → stylesheet_tag_spec.rb} +13 -13
  36. data/spec/unit/hanami/helpers/assets_helper/video_tag_spec.rb +5 -1
  37. data/spec/unit/hanami/version_spec.rb +1 -1
  38. metadata +15 -6
data/lib/hanami/slice.rb CHANGED
@@ -952,6 +952,7 @@ module Hanami
952
952
  config = self.config
953
953
  rack_monitor = self["rack.monitor"]
954
954
 
955
+ show_welcome = Hanami.env?(:development) && routes.empty?
955
956
  render_errors = render_errors?
956
957
  render_detailed_errors = render_detailed_errors?
957
958
 
@@ -971,6 +972,8 @@ module Hanami
971
972
  ) do
972
973
  use(rack_monitor)
973
974
 
975
+ use(Hanami::Web::Welcome) if show_welcome
976
+
974
977
  use(
975
978
  Hanami::Middleware::RenderErrors,
976
979
  config,
@@ -982,8 +985,15 @@ module Hanami
982
985
  use(Hanami::Webconsole::Middleware, config)
983
986
  end
984
987
 
985
- if Hanami.bundled?("hanami-controller") && config.actions.sessions.enabled?
986
- use(*config.actions.sessions.middleware)
988
+ if Hanami.bundled?("hanami-controller")
989
+ if config.actions.method_override
990
+ require "rack/method_override"
991
+ use(Rack::MethodOverride)
992
+ end
993
+
994
+ if config.actions.sessions.enabled?
995
+ use(*config.actions.sessions.middleware)
996
+ end
987
997
  end
988
998
 
989
999
  if Hanami.bundled?("hanami-assets") && config.assets.serve
@@ -46,19 +46,16 @@ module Hanami
46
46
  end
47
47
 
48
48
  def load_slices
49
- slice_configs = Dir[root.join(CONFIG_DIR, SLICES_DIR, "*#{RB_EXT}")]
50
- .map { |file| File.basename(file, RB_EXT) }
49
+ slice_configs = root.join(CONFIG_DIR, SLICES_DIR).glob("*#{RB_EXT}")
50
+ .map { _1.basename(RB_EXT) }
51
51
 
52
- slice_dirs = Dir[File.join(root, SLICES_DIR, "*")]
53
- .select { |path| File.directory?(path) }
54
- .map { |path| File.basename(path) }
52
+ slice_dirs = root.join(SLICES_DIR).glob("*")
53
+ .select(&:directory?)
54
+ .map { _1.basename }
55
55
 
56
- slice_names = (slice_dirs + slice_configs).uniq.sort
56
+ (slice_dirs + slice_configs).uniq.sort
57
57
  .then { filter_slice_names(_1) }
58
-
59
- slice_names.each do |slice_name|
60
- load_slice(slice_name)
61
- end
58
+ .each(&method(:load_slice))
62
59
 
63
60
  self
64
61
  end
@@ -97,21 +94,20 @@ module Hanami
97
94
  parent.eql?(parent.app) ? Object : parent.namespace
98
95
  end
99
96
 
100
- # Runs when a slice file has been found at `config/slices/[slice_name].rb`, or a slice directory
101
- # at `slices/[slice_name]`. Attempts to require the slice class, if defined, before registering
102
- # the slice. If a slice class is not found, registering the slice will generate the slice class.
97
+ # Runs when a slice file has been found inside the app at `config/slices/[slice_name].rb`,
98
+ # or when a slice directory exists at `slices/[slice_name]`.
99
+ #
100
+ # If a slice definition file is found by `find_slice_require_path`, then `load_slice` will
101
+ # require the file before registering the slice class.
102
+ #
103
+ # If a slice class is not found, registering the slice will generate the slice class.
103
104
  def load_slice(slice_name)
104
- slice_require_path = root.join(CONFIG_DIR, SLICES_DIR, slice_name).to_s
105
- begin
106
- require(slice_require_path)
107
- rescue LoadError => e
108
- raise e unless e.path == slice_require_path
109
- end
105
+ slice_require_path = find_slice_require_path(slice_name)
106
+ require slice_require_path if slice_require_path
110
107
 
111
- slice_module_name = inflector.camelize("#{parent_slice_namespace.name}#{PATH_DELIMITER}#{slice_name}")
112
108
  slice_class =
113
109
  begin
114
- inflector.constantize("#{slice_module_name}#{MODULE_DELIMITER}Slice")
110
+ inflector.constantize("#{slice_module_name(slice_name)}#{MODULE_DELIMITER}Slice")
115
111
  rescue NameError => e
116
112
  raise e unless e.name.to_s == inflector.camelize(slice_name) || e.name.to_s == :Slice
117
113
  end
@@ -119,11 +115,36 @@ module Hanami
119
115
  register(slice_name, slice_class)
120
116
  end
121
117
 
118
+ # Finds the path to the slice's definition file, if it exists, in the following order:
119
+ #
120
+ # 1. `config/slices/[slice_name].rb`
121
+ # 2. `slices/[parent_slice_name]/config/[slice_name].rb` (unless parent is the app)
122
+ # 3. `slices/[slice_name]/config/slice.rb`
123
+ #
124
+ # If the slice is nested under another slice then it will look in the following order:
125
+ #
126
+ # 1. `config/slices/[parent_slice_name]/[slice_name].rb`
127
+ # 2. `slices/[parent_slice_name]/config/[slice_name].rb`
128
+ # 3. `slices/[parent_slice_name]/[slice_name]/config/slice.rb`
129
+ def find_slice_require_path(slice_name)
130
+ app_slice_file_path = [slice_name]
131
+ app_slice_file_path.prepend(parent.slice_name) unless parent.eql?(parent.app)
132
+ ancestors = [
133
+ parent.app.root.join(CONFIG_DIR, SLICES_DIR, app_slice_file_path.join(File::SEPARATOR)),
134
+ parent.root.join(CONFIG_DIR, SLICES_DIR, slice_name),
135
+ root.join(SLICES_DIR, slice_name, CONFIG_DIR, "slice")
136
+ ]
137
+
138
+ ancestors
139
+ .uniq
140
+ .find { _1.sub_ext(RB_EXT).file? }
141
+ &.to_s
142
+ end
143
+
122
144
  def build_slice(slice_name, &block)
123
- slice_module_name = inflector.camelize("#{parent_slice_namespace.name}#{PATH_DELIMITER}#{slice_name}")
124
145
  slice_module =
125
146
  begin
126
- inflector.constantize(slice_module_name)
147
+ inflector.constantize(slice_module_name(slice_name))
127
148
  rescue NameError
128
149
  parent_slice_namespace.const_set(inflector.camelize(slice_name), Module.new)
129
150
  end
@@ -131,6 +152,10 @@ module Hanami
131
152
  slice_module.const_set(:Slice, Class.new(Hanami::Slice, &block))
132
153
  end
133
154
 
155
+ def slice_module_name(slice_name)
156
+ inflector.camelize("#{parent_slice_namespace.name}#{PATH_DELIMITER}#{slice_name}")
157
+ end
158
+
134
159
  def configure_slice(slice_name, slice)
135
160
  slice.instance_variable_set(:@parent, parent)
136
161
 
@@ -7,7 +7,7 @@ module Hanami
7
7
  # @api private
8
8
  module Version
9
9
  # @api public
10
- VERSION = "2.1.0.beta2"
10
+ VERSION = "2.1.0.rc1"
11
11
 
12
12
  # @since 0.9.0
13
13
  # @api private
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "delegate"
4
+ require "json"
5
+
3
6
  module Hanami
4
7
  # @api private
5
8
  module Web
@@ -53,10 +56,75 @@ module Hanami
53
56
  end
54
57
  end
55
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.last.then { |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)
110
+ payload[:message] = message if message
111
+ logger.info(JSON.fast_generate(payload))
112
+ end
113
+
114
+ # @see info
115
+ #
116
+ # @since 2.1.0
117
+ # @api private
118
+ def error(message = nil, **payload)
119
+ payload[:message] = message if message
120
+ logger.info(JSON.fast_generate(payload))
121
+ end
122
+ end
123
+
56
124
  # @api private
57
125
  # @since 2.0.0
58
126
  def initialize(logger, env: :development)
59
- @logger = logger
127
+ @logger = UniversalLogger[logger]
60
128
  extend(Development) if %i[development test].include?(env)
61
129
  end
62
130
 
@@ -77,7 +145,7 @@ module Hanami
77
145
  # @since 2.0.0
78
146
  def log_request(env, status, elapsed)
79
147
  logger.tagged(:rack) do
80
- logger.info(data(env, status: status, elapsed: elapsed))
148
+ logger.info(**data(env, status: status, elapsed: elapsed))
81
149
  end
82
150
  end
83
151
 
@@ -0,0 +1,203 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>Hanami</title>
8
+ <style>
9
+ :root {
10
+ --max-width: 1024px;
11
+ --foreground-rgb: 0, 0, 0;
12
+ --background-rgb: 255, 255, 255;
13
+ --card-border-rgb: 200, 200, 200;
14
+ --card-background-rgb: 100, 100, 100;
15
+ --font-sans: ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;
16
+ --gradient: radial-gradient(circle at 50% 125%, rgba(255,202,212,1) 0%, rgba(255,202,212,0) 40%);
17
+ }
18
+
19
+ @media (prefers-color-scheme: dark) {
20
+ :root {
21
+ --foreground-rgb: 255, 255, 255;
22
+ --background-rgb: 0, 0, 0;
23
+ --card-border-rgb: 200, 200, 200;
24
+ --card-background-rgb: 100, 100, 100;
25
+ --gradient: radial-gradient(circle at 50% 125%, rgba(255,73,108,0.75) 0%, rgba(255,73,108,0) 40%);
26
+ }
27
+ }
28
+
29
+ * {
30
+ box-sizing: border-box;
31
+ margin: 0;
32
+ padding: 0;
33
+ }
34
+
35
+ body,
36
+ html {
37
+ max-width: 100vw;
38
+ overflow-x: hidden;
39
+ font-size: 100%;
40
+ }
41
+
42
+ body {
43
+ color: rgb(var(--foreground-rgb));
44
+ background: var(--gradient) rgb(var(--background-rgb));
45
+ font-family: var(--font-sans);
46
+ font-style: normal;
47
+ }
48
+
49
+ main {
50
+ display: flex;
51
+ flex-direction: column;
52
+ align-items: center;
53
+ padding: 2rem 8vw;
54
+ min-height: 100vh;
55
+ }
56
+
57
+ a {
58
+ text-decoration: none;
59
+ color: rgb(var(--foreground-rgb));
60
+ }
61
+
62
+ .welcome {
63
+ display: flex;
64
+ flex-direction: column;
65
+ gap: 2vh;
66
+ justify-content: center;
67
+ flex-grow: 3;
68
+ align-items: center;
69
+ text-align: center;
70
+ position: relative;
71
+ margin-bottom: 4vh;
72
+ }
73
+
74
+ .welcome .logo {
75
+ display: block;
76
+ width: 120px;
77
+ height: 120px;
78
+ }
79
+
80
+ .welcome h1 {
81
+ font-size: 2.625rem;
82
+ font-weight: 500;
83
+ }
84
+
85
+ .grid {
86
+ display: grid;
87
+ grid-template-columns: repeat(4, 1fr);
88
+ column-gap: 20px;
89
+ max-width: 100%;
90
+ margin-bottom: 8vh;
91
+ width: var(--max-width);
92
+ }
93
+
94
+ .card {
95
+ padding: 1rem;
96
+ border-radius: 12px;
97
+ border: 1px solid rgba(var(--card-border-rgb), 0.3);
98
+ background: rgba(var(--card-background-rgb), 0);
99
+ transition: background 200ms, border 200ms;
100
+ }
101
+
102
+ .card:hover {
103
+ background: rgba(var(--card-background-rgb), 0.05);
104
+ }
105
+
106
+ .card h2 {
107
+ font-size: 1.5rem;
108
+ font-weight: 500;
109
+ margin-bottom: 0.8rem;
110
+ }
111
+
112
+ .card p {
113
+ line-height: 1.5;
114
+ opacity: 0.6;
115
+ }
116
+
117
+ .meta {
118
+ text-align: center;
119
+ opacity: 50%;
120
+ }
121
+
122
+ /* Mobile */
123
+ @media (max-width: 700px) {
124
+
125
+ .welcome {
126
+ justify-content: top;
127
+ flex-grow: 0;
128
+ margin-bottom: 4rem;
129
+ }
130
+
131
+ .welcome .logo {
132
+ width: 90px;
133
+ height: 90px;
134
+ }
135
+
136
+ .welcome h1 {
137
+ font-size: 2rem;
138
+ }
139
+
140
+ .grid {
141
+ grid-template-columns: 1fr;
142
+ row-gap: 20px;
143
+ }
144
+
145
+ .card {
146
+ text-align: center;
147
+ }
148
+ }
149
+
150
+ /* Tablet and Smaller Desktop */
151
+ @media (min-width: 701px) and (max-width: 1120px) {
152
+ .grid {
153
+ grid-template-columns: repeat(2, 1fr);
154
+ gap: 20px;
155
+ }
156
+ }
157
+
158
+ @media (prefers-color-scheme: dark) {
159
+ html {
160
+ color-scheme: dark;
161
+ }
162
+ .card {
163
+ border: 1px solid rgba(var(--card-border-rgb), 0.15);
164
+ background: rgba(var(--card-background-rgb), 0);
165
+ }
166
+
167
+ .card:hover {
168
+ background: rgba(var(--card-background-rgb), 0.1);
169
+ }
170
+ }
171
+
172
+ </style>
173
+ </head>
174
+ <body>
175
+ <main>
176
+ <div class="welcome">
177
+ <img src="data:image/svg+xml,%3Csvg height='840' width='840' viewBox='0 0 840 840' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Cmask id='a' fill='%23fff'%3E%3Cpath d='m0 0h840v840h-840z' fill='%23fff' fill-rule='evenodd'/%3E%3C/mask%3E%3Cg fill='none' fill-rule='evenodd' mask='url(%23a)'%3E%3Cg transform='translate(-115.9919 -103.9919)'%3E%3Cg fill='%23dc3610'%3E%3Cpath d='m523.491853 126.991853 377.093909 273.974762-144.037057 443.300476h-466.113705l-144.037056-443.300476z' opacity='.6' transform='matrix(.91354546 .40673664 -.40673664 .91354546 258.181591 -167.665085)'/%3E%3Cpath d='m525.491853 129.991853 377.093909 273.974762-144.037057 443.300476h-466.113705l-144.037056-443.300476z' opacity='.8' transform='matrix(.97437006 .22495105 -.22495105 .97437006 131.903231 -104.716004)'/%3E%3Cpath d='m535.491853 130.991853 377.093909 273.974762-144.037057 443.300476h-466.113705l-144.037056-443.300476z'/%3E%3C/g%3E%3Cpath d='m534.991853 370.991853c86.156421 0 156 69.843579 156 156s-69.843579 156-156 156-156-69.843579-156-156 69.843579-156 156-156zm0 35c-66.826455 0-121 54.173545-121 121s54.173545 121 121 121 121-54.173545 121-121-54.173545-121-121-121z' fill='%23fff'/%3E%3Cpath d='m535.947441 478.991853 74.023116 90.999216-20.023116 27h-108l-19.978704-27z' fill='%23fff' transform='matrix(-1 0 0 -1 1071.9392 1075.983)'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E" alt="Hanani logo" class="logo">
178
+ <h1>Welcome to Hanami</h1>
179
+ </div>
180
+ <div class="grid">
181
+ <a href="https://guides.hanamirb.org" class="card">
182
+ <h2>Guides</h2>
183
+ <p>Get started with the Hanami guides</p>
184
+ </a>
185
+ <a href="https://docs.hanamirb.org/" class="card">
186
+ <h2>API docs</h2>
187
+ <p>Learn more through the API docs</p>
188
+ </a>
189
+ <a href="http://github.com/hanami" class="card">
190
+ <h2>Code</h2>
191
+ <p>Contribute to the source code</p>
192
+ </a>
193
+ <a href="https://discourse.hanamirb.org" class="card">
194
+ <h2>Forum</h2>
195
+ <p>Join the conversation on the forum</p>
196
+ </a>
197
+ </div>
198
+ <p class="meta">
199
+ Hanami version: <%= hanami_version %> • Ruby version: <%= ruby_version %>
200
+ </p>
201
+ </main>
202
+ </body>
203
+ </html>
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "erb"
4
+
5
+ module Hanami
6
+ # @api private
7
+ module Web
8
+ # Middleware that renders a welcome view in fresh Hanami apps.
9
+ #
10
+ # @api private
11
+ # @since 2.1.0
12
+ class Welcome
13
+ # @api private
14
+ # @since 2.1.0
15
+ def initialize(app)
16
+ @app = app
17
+ end
18
+
19
+ # @api private
20
+ # @since 2.1.0
21
+ def call(env)
22
+ request_path = env["REQUEST_PATH"] || ""
23
+ request_host = env["HTTP_HOST"] || ""
24
+
25
+ template_path = File.join(__dir__, "welcome.html.erb")
26
+ body = [ERB.new(File.read(template_path)).result(binding)]
27
+
28
+ [200, {}, body]
29
+ end
30
+
31
+ private
32
+
33
+ # @api private
34
+ # @since 2.1.0
35
+ def hanami_version
36
+ Hanami::VERSION
37
+ end
38
+
39
+ # @api private
40
+ # @since 2.1.0
41
+ def ruby_version
42
+ RUBY_DESCRIPTION
43
+ end
44
+ end
45
+ end
46
+ end
@@ -18,6 +18,19 @@ RSpec.describe "Assets", :app_integration do
18
18
  end
19
19
  RUBY
20
20
 
21
+ write "config/assets.mjs", <<~JS
22
+ import * as assets from "hanami-assets";
23
+ await assets.run();
24
+ JS
25
+
26
+ write "package.json", <<~JSON
27
+ {
28
+ "scripts": {
29
+ "assets": "node config/assets.mjs"
30
+ }
31
+ }
32
+ JSON
33
+
21
34
  write "config/routes.rb", <<~RUBY
22
35
  module TestApp
23
36
  class Routes < Hanami::Routes
@@ -62,10 +75,8 @@ RSpec.describe "Assets", :app_integration do
62
75
  RUBY
63
76
 
64
77
  write "app/templates/posts/show.html.erb", <<~ERB
65
- <%= stylesheet_link_tag("app") %>
66
- <%= css("app") %>
78
+ <%= stylesheet_tag("app") %>
67
79
  <%= javascript_tag("app") %>
68
- <%= js("app") %>
69
80
  ERB
70
81
 
71
82
  write "app/assets/js/app.ts", <<~TS
@@ -11,6 +11,8 @@ RSpec.describe "Logging / Request logging", :app_integration do
11
11
 
12
12
  let(:logger_stream) { StringIO.new }
13
13
 
14
+ let(:root) { make_tmp_directory }
15
+
14
16
  def configure_logger
15
17
  Hanami.app.config.logger.stream = logger_stream
16
18
  end
@@ -19,14 +21,18 @@ RSpec.describe "Logging / Request logging", :app_integration do
19
21
  @logs ||= (logger_stream.rewind and logger_stream.read)
20
22
  end
21
23
 
24
+ def generate_app
25
+ write "config/app.rb", <<~RUBY
26
+ module TestApp
27
+ class App < Hanami::App
28
+ end
29
+ end
30
+ RUBY
31
+ end
32
+
22
33
  before do
23
- with_directory(make_tmp_directory) do
24
- write "config/app.rb", <<~RUBY
25
- module TestApp
26
- class App < Hanami::App
27
- end
28
- end
29
- RUBY
34
+ with_directory(root) do
35
+ generate_app
30
36
 
31
37
  require "hanami/setup"
32
38
  configure_logger
@@ -125,4 +131,56 @@ RSpec.describe "Logging / Request logging", :app_integration do
125
131
  end
126
132
  end
127
133
  end
134
+
135
+ context "when using ::Logger from Ruby stdlib" do
136
+ def generate_app
137
+ write "config/app.rb", <<~RUBY
138
+ require "logger"
139
+ require "pathname"
140
+
141
+ module TestApp
142
+ class App < Hanami::App
143
+ stream = Pathname.new(#{root.to_s.inspect}).join("log").tap(&:mkpath).join("test.log").to_s
144
+ config.logger = ::Logger.new(stream, progname: "custom-logger-app")
145
+ end
146
+ end
147
+ RUBY
148
+ end
149
+
150
+ def before_prepare
151
+ with_directory(root) do
152
+ write "config/routes.rb", <<~RUBY
153
+ module TestApp
154
+ class Routes < Hanami::Routes
155
+ root to: ->(env) { [200, {}, ["OK"]] }
156
+ end
157
+ end
158
+ RUBY
159
+ end
160
+ end
161
+
162
+ let(:logs) do
163
+ Pathname.new(root).join("log", "test.log").readlines
164
+ end
165
+
166
+ it "logs the requests with the payload serialized as JSON" do
167
+ get "/"
168
+
169
+ request_log = logs.last
170
+
171
+ # Expected log line follows the standard Logger structure:
172
+ #
173
+ # I, [2023-10-14T14:55:16.638753 #94836] INFO -- custom-logger-app: {"verb":"GET", ...}
174
+ expect(request_log).to match(%r{INFO -- custom-logger-app:})
175
+
176
+ # The log message should be JSON, after the progname
177
+ log_message = request_log.split("custom-logger-app: ").last
178
+ log_payload = JSON.parse(log_message, symbolize_names: true)
179
+
180
+ expect(log_payload).to include(
181
+ verb: "GET",
182
+ status: 200
183
+ )
184
+ end
185
+ end
128
186
  end