hanami 2.1.0.beta2 → 2.1.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
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