hanami 2.1.0.beta2.1 → 2.1.0.rc2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|