hanami 2.1.0.beta2.1 → 2.1.0.rc2
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 +30 -0
- data/hanami.gemspec +3 -3
- data/lib/hanami/config/actions.rb +16 -3
- data/lib/hanami/config/assets.rb +1 -1
- data/lib/hanami/config/views.rb +10 -2
- data/lib/hanami/config.rb +21 -12
- data/lib/hanami/extensions/action/slice_configured_action.rb +5 -5
- data/lib/hanami/extensions/action.rb +4 -4
- data/lib/hanami/extensions/view/context.rb +111 -19
- data/lib/hanami/extensions/view/part.rb +64 -3
- data/lib/hanami/extensions/view/scope.rb +7 -0
- data/lib/hanami/extensions/view/slice_configured_context.rb +12 -8
- data/lib/hanami/extensions/view/slice_configured_helpers.rb +12 -1
- data/lib/hanami/extensions/view/slice_configured_part.rb +71 -0
- data/lib/hanami/extensions/view/slice_configured_view.rb +14 -6
- data/lib/hanami/extensions/view/standard_helpers.rb +4 -0
- data/lib/hanami/extensions/view.rb +5 -3
- data/lib/hanami/helpers/assets_helper.rb +47 -79
- data/lib/hanami/routes.rb +33 -2
- data/lib/hanami/slice.rb +12 -2
- data/lib/hanami/slice_registrar.rb +48 -23
- data/lib/hanami/version.rb +1 -1
- data/lib/hanami/web/rack_logger.rb +70 -2
- data/lib/hanami/web/welcome.html.erb +203 -0
- data/lib/hanami/web/welcome.rb +46 -0
- data/spec/integration/assets/assets_spec.rb +14 -3
- data/spec/integration/logging/request_logging_spec.rb +65 -7
- data/spec/integration/rack_app/method_override_spec.rb +97 -0
- data/spec/integration/slices_spec.rb +275 -5
- data/spec/integration/view/context/assets_spec.rb +0 -8
- data/spec/integration/view/context/inflector_spec.rb +0 -8
- data/spec/integration/view/context/settings_spec.rb +0 -8
- data/spec/integration/view/helpers/part_helpers_spec.rb +2 -2
- data/spec/integration/view/helpers/user_defined_helpers/part_helpers_spec.rb +10 -10
- data/spec/integration/view/parts/default_rendering_spec.rb +138 -0
- data/spec/integration/web/welcome_view_spec.rb +84 -0
- data/spec/support/app_integration.rb +22 -4
- data/spec/unit/hanami/config/render_detailed_errors_spec.rb +1 -1
- data/spec/unit/hanami/helpers/assets_helper/{audio_spec.rb → audio_tag_spec.rb} +10 -14
- data/spec/unit/hanami/helpers/assets_helper/{favicon_spec.rb → favicon_tag_spec.rb} +7 -11
- data/spec/unit/hanami/helpers/assets_helper/{image_spec.rb → image_tag_spec.rb} +8 -12
- data/spec/unit/hanami/helpers/assets_helper/{javascript_spec.rb → javascript_tag_spec.rb} +14 -18
- data/spec/unit/hanami/helpers/assets_helper/{stylesheet_spec.rb → stylesheet_tag_spec.rb} +12 -16
- data/spec/unit/hanami/helpers/assets_helper/{video_spec.rb → video_tag_spec.rb} +11 -11
- data/spec/unit/hanami/version_spec.rb +1 -1
- metadata +28 -19
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")
|
986
|
-
|
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 =
|
50
|
-
.map {
|
49
|
+
slice_configs = root.join(CONFIG_DIR, SLICES_DIR).glob("*#{RB_EXT}")
|
50
|
+
.map { _1.basename(RB_EXT) }
|
51
51
|
|
52
|
-
slice_dirs =
|
53
|
-
.select
|
54
|
-
.map {
|
52
|
+
slice_dirs = root.join(SLICES_DIR).glob("*")
|
53
|
+
.select(&:directory?)
|
54
|
+
.map { _1.basename }
|
55
55
|
|
56
|
-
|
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`,
|
101
|
-
# at `slices/[slice_name]`.
|
102
|
-
#
|
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 =
|
105
|
-
|
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
|
|
data/lib/hanami/version.rb
CHANGED
@@ -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
|
-
<%=
|
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(
|
24
|
-
|
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
|