hanami 2.1.0.beta2.1 → 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 +25 -0
  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 +6 -38
  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_spec.rb → audio_tag_spec.rb} +10 -14
  32. data/spec/unit/hanami/helpers/assets_helper/{favicon_spec.rb → favicon_tag_spec.rb} +7 -11
  33. data/spec/unit/hanami/helpers/assets_helper/{image_spec.rb → image_tag_spec.rb} +8 -12
  34. data/spec/unit/hanami/helpers/assets_helper/{javascript_spec.rb → javascript_tag_spec.rb} +14 -18
  35. data/spec/unit/hanami/helpers/assets_helper/{stylesheet_spec.rb → stylesheet_tag_spec.rb} +12 -16
  36. data/spec/unit/hanami/helpers/assets_helper/{video_spec.rb → video_tag_spec.rb} +11 -11
  37. data/spec/unit/hanami/version_spec.rb +1 -1
  38. metadata +23 -14
@@ -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
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rack/test"
4
+ require "stringio"
5
+
6
+ RSpec.describe "Hanami web app: Method Override", :app_integration do
7
+ include Rack::Test::Methods
8
+
9
+ let(:app) { Hanami.app }
10
+
11
+ around do |example|
12
+ with_tmp_directory(Dir.mktmpdir, &example)
13
+ end
14
+
15
+ context "enabled by default" do
16
+ before do
17
+ write "config/app.rb", <<~RUBY
18
+ require "hanami"
19
+
20
+ module TestApp
21
+ class App < Hanami::App
22
+ config.logger.stream = StringIO.new
23
+ end
24
+ end
25
+ RUBY
26
+
27
+ generate_app_code
28
+ end
29
+
30
+ it "overrides the original HTTP method" do
31
+ post(
32
+ "/users/:id",
33
+ {"_method" => "PUT"},
34
+ "CONTENT_TYPE" => "multipart/form-data"
35
+ )
36
+
37
+ expect(last_response).to be_successful
38
+ expect(last_response.body).to eq("PUT")
39
+ end
40
+ end
41
+
42
+ context "when disabled" do
43
+ before do
44
+ write "config/app.rb", <<~RUBY
45
+ require "hanami"
46
+
47
+ module TestApp
48
+ class App < Hanami::App
49
+ config.logger.stream = StringIO.new
50
+ config.actions.method_override = false
51
+ end
52
+ end
53
+ RUBY
54
+
55
+ generate_app_code
56
+ end
57
+
58
+ it "overrides the original HTTP method" do
59
+ post(
60
+ "/users/:id",
61
+ {"_method" => "PUT"},
62
+ "CONTENT_TYPE" => "multipart/form-data"
63
+ )
64
+
65
+ expect(last_response).to_not be_successful
66
+ expect(last_response.status).to be(404)
67
+ end
68
+ end
69
+
70
+ private
71
+
72
+ def generate_app_code
73
+ write "config/routes.rb", <<~RUBY
74
+ module TestApp
75
+ class Routes < Hanami::Routes
76
+ put "/users/:id", to: "users.update"
77
+ end
78
+ end
79
+ RUBY
80
+
81
+ write "app/actions/users/update.rb", <<~RUBY
82
+ module TestApp
83
+ module Actions
84
+ module Users
85
+ class Update < Hanami::Action
86
+ def handle(req, res)
87
+ res.body = req.env.fetch("REQUEST_METHOD")
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
93
+ RUBY
94
+
95
+ require "hanami/boot"
96
+ end
97
+ end