lennarb 0.1.7 → 0.2.0
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 +21 -0
- data/exe/lenna +2 -4
- data/lib/lennarb/request.rb +37 -0
- data/lib/lennarb/response.rb +138 -0
- data/lib/lennarb/route_node.rb +73 -0
- data/lib/lennarb/version.rb +3 -3
- data/lib/lennarb.rb +78 -27
- data/license.md +2 -1
- data/readme.md +30 -13
- metadata +42 -57
- data/lib/lenna/application.rb +0 -53
- data/lib/lenna/cli/app.rb +0 -39
- data/lib/lenna/cli/commands/create_project.rb +0 -125
- data/lib/lenna/cli/commands/interface.rb +0 -20
- data/lib/lenna/cli/commands/start_server.rb +0 -143
- data/lib/lenna/cli/templates/application.erb +0 -11
- data/lib/lenna/cli/templates/config.ru.erb +0 -5
- data/lib/lenna/cli/templates/gemfile.erb +0 -14
- data/lib/lenna/middleware/app.rb +0 -118
- data/lib/lenna/middleware/default/error_handler.rb +0 -243
- data/lib/lenna/middleware/default/logging.rb +0 -93
- data/lib/lenna/middleware/default/reload.rb +0 -97
- data/lib/lenna/router/builder.rb +0 -124
- data/lib/lenna/router/cache.rb +0 -52
- data/lib/lenna/router/namespace_stack.rb +0 -77
- data/lib/lenna/router/request.rb +0 -141
- data/lib/lenna/router/response.rb +0 -509
- data/lib/lenna/router/route_matcher.rb +0 -68
- data/lib/lenna/router.rb +0 -206
- data/lib/lennarb/array_extensions.rb +0 -31
@@ -1,243 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
# Released under the MIT License.
|
4
|
-
# Copyright, 2023, by Aristóteles Coutinho.
|
5
|
-
|
6
|
-
require 'cgi'
|
7
|
-
|
8
|
-
module Lenna
|
9
|
-
module Middleware
|
10
|
-
module Default
|
11
|
-
# This middleware will handle errors.
|
12
|
-
#
|
13
|
-
# @private Since `v0.1.0`
|
14
|
-
#
|
15
|
-
module ErrorHandler
|
16
|
-
extend self
|
17
|
-
|
18
|
-
# This method will be called by the server.
|
19
|
-
#
|
20
|
-
# @parameter req [Rack::Request] The request object
|
21
|
-
# @parameter res [Rack::Response] The response object
|
22
|
-
# @parameter next_middleware [Proc] The next middleware in the stack
|
23
|
-
#
|
24
|
-
# @return [void]
|
25
|
-
#
|
26
|
-
def call(req, res, next_middleware)
|
27
|
-
next_middleware.call
|
28
|
-
rescue StandardError => e
|
29
|
-
env = req.env
|
30
|
-
log_error(env, e)
|
31
|
-
|
32
|
-
render_error_page(e, req, res)
|
33
|
-
end
|
34
|
-
|
35
|
-
private
|
36
|
-
|
37
|
-
# This method will render the error page.
|
38
|
-
#
|
39
|
-
# @parameter error [StandardError] The error object
|
40
|
-
# @parameter env [Hash] The environment variables
|
41
|
-
# @parameter res [Rack::Response] The response object
|
42
|
-
#
|
43
|
-
# @return [void]
|
44
|
-
#
|
45
|
-
def render_error_page(error, req, res)
|
46
|
-
case req.headers['Content-Type']
|
47
|
-
in 'application/json' then render_json(error, res)
|
48
|
-
else render_html(error, res)
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
52
|
-
# This method will render the JSON error page.
|
53
|
-
#
|
54
|
-
# @parameter error [StandardError] The error object
|
55
|
-
# @parameter env [Hash] The environment variables
|
56
|
-
# @parameter res [Rack::Response] The response object
|
57
|
-
#
|
58
|
-
# @return [void]
|
59
|
-
#
|
60
|
-
def render_json(error, res)
|
61
|
-
case ENV.fetch('RACK_ENV', 'development')
|
62
|
-
in 'development' | 'test' then res.json(
|
63
|
-
data: {
|
64
|
-
error: error.message,
|
65
|
-
status: 500
|
66
|
-
}
|
67
|
-
)
|
68
|
-
else res.json(data: { error: 'Internal Server Error', status: 500 })
|
69
|
-
end
|
70
|
-
end
|
71
|
-
|
72
|
-
# This method will render the HTML error page.
|
73
|
-
#
|
74
|
-
# @parameter error [StandardError] The error object
|
75
|
-
# @parameter env [Hash] The environment variables
|
76
|
-
# @parameter res [Rack::Response] The response object
|
77
|
-
#
|
78
|
-
# @return [void]
|
79
|
-
#
|
80
|
-
def render_html(error, res)
|
81
|
-
res.html(error_page(error), status: 500)
|
82
|
-
end
|
83
|
-
|
84
|
-
# This method will log the error.
|
85
|
-
#
|
86
|
-
# @parameter env [Hash] The environment variables
|
87
|
-
# @parameter error [StandardError] The error object
|
88
|
-
#
|
89
|
-
# @return [void]
|
90
|
-
#
|
91
|
-
def log_error(env, error)
|
92
|
-
env['rack.errors'].puts error.message
|
93
|
-
env['rack.errors'].puts error.backtrace.join("\n")
|
94
|
-
env['rack.errors'].flush
|
95
|
-
end
|
96
|
-
|
97
|
-
# This method will render the error page.
|
98
|
-
#
|
99
|
-
def error_page(error)
|
100
|
-
style = <<-STYLE
|
101
|
-
<style>
|
102
|
-
body { font-family: 'Helvetica Neue', sans-serif; background-color: #F7F7F7; color: #333; margin: 0; padding: 0; }
|
103
|
-
.error-container { max-width: 600px; margin: 20px auto; padding: 20px; box-shadow: 0 4px 8px 0 rgba(0,0,0,0.1); background: white; border-radius: 4px; }
|
104
|
-
h1 { color: #c0392b }
|
105
|
-
h2 { padding: 0; margin: 0; font-size: 1.2em; }
|
106
|
-
.error-details, .backtrace { text-align: left; }
|
107
|
-
.error-details strong { color: #e74c3c; }
|
108
|
-
pre { background: #ecf0f1; padding: 15px; overflow: auto; border-left: 5px solid #e74c3c; font-size: 0.9em; }
|
109
|
-
.backtrace { margin-top: 20px; }
|
110
|
-
.backtrace pre { border-color: #3498db; }
|
111
|
-
svg { fill: #e74c3c; width: 50px; height: auto; }
|
112
|
-
.container { display: flex; justify-content: space-between; align-items: center; align-content: center;}
|
113
|
-
</style>
|
114
|
-
STYLE
|
115
|
-
|
116
|
-
# SVG logo
|
117
|
-
svg_logo = <<~SVG
|
118
|
-
<svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="100" height="100" viewBox="0 0 100 100">
|
119
|
-
<path fill="#f1bc19" d="M77 12A1 1 0 1 0 77 14A1 1 0 1 0 77 12Z"></path><path fill="#e4e4f9" d="M50 13A37 37 0 1 0 50 87A37 37 0 1 0 50 13Z"></path><path fill="#f1bc19" d="M83 11A4 4 0 1 0 83 19A4 4 0 1 0 83 11Z"></path><path fill="#8889b9" d="M87 22A2 2 0 1 0 87 26A2 2 0 1 0 87 22Z"></path><path fill="#fbcd59" d="M81 74A2 2 0 1 0 81 78 2 2 0 1 0 81 74zM15 59A4 4 0 1 0 15 67 4 4 0 1 0 15 59z"></path><path fill="#8889b9" d="M25 85A2 2 0 1 0 25 89A2 2 0 1 0 25 85Z"></path><path fill="#fff" d="M18.5 49A2.5 2.5 0 1 0 18.5 54 2.5 2.5 0 1 0 18.5 49zM79.5 32A1.5 1.5 0 1 0 79.5 35 1.5 1.5 0 1 0 79.5 32z"></path><g><path fill="#fdfcee" d="M50 25.599999999999998A24.3 24.3 0 1 0 50 74.2A24.3 24.3 0 1 0 50 25.599999999999998Z"></path><path fill="#472b29" d="M50,74.8c-13.8,0-25-11.2-25-25c0-13.8,11.2-25,25-25c13.8,0,25,11.2,25,25C75,63.6,63.8,74.8,50,74.8z M50,26.3c-13,0-23.5,10.6-23.5,23.5S37,73.4,50,73.4s23.5-10.6,23.5-23.5S63,26.3,50,26.3z"></path></g><g><path fill="#ea5167" d="M49.9 29.6A20.4 20.4 0 1 0 49.9 70.4A20.4 20.4 0 1 0 49.9 29.6Z"></path></g><g><path fill="#ef7d99" d="M50.2,32.9c10.6,0,19.3,8.2,20.1,18.5c0-0.5,0.1-1,0.1-1.5c0-11-9-20-20.2-20c-11.1,0-20.2,9-20.2,20 c0,0.5,0,1,0.1,1.5C30.9,41,39.5,32.9,50.2,32.9z"></path></g><g><path fill="#472b29" d="M69.4,44.6c-0.2,0-0.4-0.1-0.5-0.4c-0.1-0.3-0.2-0.6-0.3-0.9c-0.4-1.1-0.9-2.2-1.5-3.2 c-0.1-0.2-0.1-0.5,0.2-0.7c0.2-0.1,0.5-0.1,0.7,0.2c0.6,1.1,1.1,2.2,1.5,3.3c0.1,0.3,0.2,0.6,0.3,0.9c0.1,0.3-0.1,0.5-0.3,0.6 C69.5,44.6,69.5,44.6,69.4,44.6z"></path></g><g><path fill="#472b29" d="M50,70.8c-11.5,0-20.9-9.3-20.9-20.8c0-11.5,9.4-20.8,20.9-20.8c6,0,11.7,2.6,15.6,7c0.3,0.3,0.6,0.7,0.9,1 c0.2,0.2,0.1,0.5-0.1,0.7c-0.2,0.2-0.5,0.1-0.7-0.1c-0.3-0.3-0.5-0.7-0.8-1c-3.8-4.2-9.2-6.7-14.9-6.7c-11,0-19.9,8.9-19.9,19.8 c0,10.9,8.9,19.8,19.9,19.8s19.9-8.9,19.9-19.8c0-1-0.1-2-0.2-3c0-0.3,0.1-0.5,0.4-0.6c0.3,0,0.5,0.1,0.6,0.4 c0.2,1,0.2,2.1,0.2,3.1C70.9,61.4,61.5,70.8,50,70.8z"></path></g><g><path fill="#fdfcee" d="M56,57.1c-0.3,0-0.6-0.1-0.9-0.4l-5.2-5.2l-5.2,5.2c-0.2,0.2-0.5,0.4-0.9,0.4s-0.6-0.1-0.9-0.4 c-0.5-0.5-0.5-1.2,0-1.7l5.2-5.2L43,44.6c-0.5-0.5-0.5-1.2,0-1.7c0.2-0.2,0.5-0.4,0.9-0.4s0.6,0.1,0.9,0.4l5.2,5.2l5.2-5.2 c0.2-0.2,0.5-0.4,0.9-0.4c0.3,0,0.6,0.1,0.9,0.4s0.4,0.5,0.4,0.9s-0.1,0.6-0.4,0.9l-5.2,5.2l5.2,5.2c0.2,0.2,0.4,0.5,0.4,0.9 c0,0.3-0.1,0.6-0.4,0.9S56.3,57.1,56,57.1z"></path><path fill="#472b29" d="M56,43.1c0.2,0,0.4,0.1,0.5,0.2c0.3,0.3,0.3,0.7,0,1l-5.5,5.5l5.5,5.5c0.3,0.3,0.3,0.7,0,1 c-0.1,0.1-0.3,0.2-0.5,0.2s-0.4-0.1-0.5-0.2l-5.5-5.5l-5.5,5.5c-0.1,0.1-0.3,0.2-0.5,0.2s-0.4-0.1-0.5-0.2c-0.3-0.3-0.3-0.7,0-1 l5.5-5.5l-5.5-5.5c-0.3-0.3-0.3-0.7,0-1c0.1-0.1,0.3-0.2,0.5-0.2s0.4,0.1,0.5,0.2l5.5,5.5l5.5-5.5C55.6,43.1,55.8,43.1,56,43.1 M56,42.1c-0.5,0-0.9,0.2-1.2,0.5l-4.8,4.8l-4.8-4.8c-0.3-0.3-0.8-0.5-1.2-0.5s-0.9,0.2-1.2,0.5c-0.3,0.3-0.5,0.8-0.5,1.2 s0.2,0.9,0.5,1.2l4.8,4.8l-4.8,4.8c-0.3,0.3-0.5,0.8-0.5,1.2s0.2,0.9,0.5,1.2c0.3,0.3,0.8,0.5,1.2,0.5s0.9-0.2,1.2-0.5l4.8-4.8 l4.8,4.8c0.3,0.3,0.8,0.5,1.2,0.5s0.9-0.2,1.2-0.5c0.3-0.3,0.5-0.8,0.5-1.2s-0.2-0.9-0.5-1.2l-4.8-4.8l4.8-4.8 c0.3-0.3,0.5-0.8,0.5-1.2s-0.2-0.9-0.5-1.2C56.9,42.2,56.4,42.1,56,42.1L56,42.1z"></path></g>
|
120
|
-
</svg>
|
121
|
-
#{' '}
|
122
|
-
SVG
|
123
|
-
|
124
|
-
# HTML page
|
125
|
-
<<-HTML
|
126
|
-
<!DOCTYPE html>
|
127
|
-
<html lang="en">
|
128
|
-
<head>
|
129
|
-
<meta charset="UTF-8">
|
130
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
131
|
-
#{' '}
|
132
|
-
<title>System Error</title>
|
133
|
-
#{style}
|
134
|
-
</head>
|
135
|
-
<body>
|
136
|
-
<div class="error-container">
|
137
|
-
<div class="container">
|
138
|
-
<div class="item-left">
|
139
|
-
<h1>Oops! An error has occurred.</h1>
|
140
|
-
</div>
|
141
|
-
<div class="item-right">#{svg_logo}</div>
|
142
|
-
</div>
|
143
|
-
#{' '}
|
144
|
-
#{error_message_and_backtrace(error)}
|
145
|
-
</div>
|
146
|
-
</body>
|
147
|
-
</html>
|
148
|
-
HTML
|
149
|
-
end
|
150
|
-
|
151
|
-
# This method will render the error message and backtrace.
|
152
|
-
#
|
153
|
-
# @parameter error [StandardError] The error object
|
154
|
-
# @parameter env [Hash] The environment variables
|
155
|
-
#
|
156
|
-
# @return [String] The HTML string
|
157
|
-
#
|
158
|
-
def error_message_and_backtrace(error)
|
159
|
-
if ENV['RACK_ENV'] == 'development'
|
160
|
-
truncated_message =
|
161
|
-
error.message[0..500] + (error.message.length > 500 ? '...' : '')
|
162
|
-
|
163
|
-
file, line = error.backtrace.first.split(':')
|
164
|
-
line_number = Integer(line)
|
165
|
-
<<-DETAILS
|
166
|
-
<div class="error-details">
|
167
|
-
<h2>Error Details:</h2>
|
168
|
-
<p>
|
169
|
-
<strong>Message:</strong> <span id="error-message">#{CGI.escapeHTML(truncated_message)}</span>
|
170
|
-
</p>
|
171
|
-
<div>
|
172
|
-
<p><strong>Location:</strong> #{CGI.escapeHTML(file)}:#{line_number}</p>
|
173
|
-
<pre>#{extract_source(file, line_number)}</pre>
|
174
|
-
</div>
|
175
|
-
|
176
|
-
<details>
|
177
|
-
<summary>Details</summary>
|
178
|
-
<p id="full-message" style="display: none;">
|
179
|
-
<h3>Full Backtrace:</h3>
|
180
|
-
<p><strong>Full Message:</strong> #{CGI.escapeHTML(error.message)}</p>
|
181
|
-
<pre>#{error.backtrace.join("\n")}</pre>
|
182
|
-
</p>
|
183
|
-
</details>
|
184
|
-
</div>
|
185
|
-
DETAILS
|
186
|
-
else
|
187
|
-
"<p>We're sorry, but something went wrong. We've been notified " \
|
188
|
-
'about this issue and will take a look at it shortly.</p>'
|
189
|
-
end
|
190
|
-
end
|
191
|
-
|
192
|
-
# This method will extract the source code.
|
193
|
-
#
|
194
|
-
# @parameter file [String] The file path
|
195
|
-
# @parameter line_number [Integer] The line number
|
196
|
-
#
|
197
|
-
# @return [String] The HTML string
|
198
|
-
#
|
199
|
-
# ex.
|
200
|
-
# extract_source('/path/to/file.rb', 10)
|
201
|
-
# # => "<strong style='color: red;'> 7: </strong> =>
|
202
|
-
# def foo\n<strong style='color: red;'> 8: </strong>
|
203
|
-
# puts 'bar'\n<strong style='color: red;'> 9: </strong>
|
204
|
-
# end\n<strong style='color: red;'> 10: </strong> foo\n<strong
|
205
|
-
# style='color: red;'> 11: </strong> "
|
206
|
-
#
|
207
|
-
def extract_source(file, line_number)
|
208
|
-
lines = ::File.readlines(file)
|
209
|
-
start_line = [line_number - 3, 0].max
|
210
|
-
end_line = [line_number + 3, lines.size].min
|
211
|
-
|
212
|
-
line_ranger = lines[start_line...end_line]
|
213
|
-
|
214
|
-
format_lines(line_ranger, line_number).join
|
215
|
-
end
|
216
|
-
|
217
|
-
# This method will format the lines.
|
218
|
-
#
|
219
|
-
# ex.
|
220
|
-
# format_lines(line_ranger, line_number)
|
221
|
-
# # => ["<strong style='color: red;'> 7: </strong> =>\n",
|
222
|
-
# "<strong style='color: red;'> 8: </strong> puts 'bar'\n",
|
223
|
-
# "<strong style='color: red;'> 9: </strong> end\n",
|
224
|
-
# "<strong style='color: red;'> 10: </strong> foo\n",
|
225
|
-
# "<strong style='color: red;'> 11: </strong> "]
|
226
|
-
#
|
227
|
-
def format_lines(lines, highlight_line)
|
228
|
-
lines.map.with_index(highlight_line - 3 + 1) do |line, line_num|
|
229
|
-
line_number_text = "#{line_num.to_s.rjust(6)}: "
|
230
|
-
formatted_line = ::CGI.escapeHTML(line)
|
231
|
-
|
232
|
-
if line_num == highlight_line
|
233
|
-
"<strong style='color: red;'>#{line_number_text}</strong> " \
|
234
|
-
"#{formatted_line}"
|
235
|
-
else
|
236
|
-
"#{line_number_text}#{formatted_line}"
|
237
|
-
end
|
238
|
-
end
|
239
|
-
end
|
240
|
-
end
|
241
|
-
end
|
242
|
-
end
|
243
|
-
end
|
@@ -1,93 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
# Released under the MIT License.
|
4
|
-
# Copyright, 2023, by Aristóteles Coutinho.
|
5
|
-
|
6
|
-
require 'colorize'
|
7
|
-
|
8
|
-
module Lenna
|
9
|
-
module Middleware
|
10
|
-
module Default
|
11
|
-
# The Logging module is responsible for logging the requests.
|
12
|
-
#
|
13
|
-
# @private
|
14
|
-
#
|
15
|
-
module Logging
|
16
|
-
extend self
|
17
|
-
|
18
|
-
# This method is used to log the request.
|
19
|
-
#
|
20
|
-
# @parameter req [Rack::Request ] The request
|
21
|
-
# @parameter res [Rack::Response] The response
|
22
|
-
# @parameter next_middleware [Proc] The next middleware
|
23
|
-
#
|
24
|
-
def call(req, res, next_middleware)
|
25
|
-
start_time = ::Time.now
|
26
|
-
next_middleware.call
|
27
|
-
end_time = ::Time.now
|
28
|
-
|
29
|
-
http_method = colorize_http_method(req.request_method)
|
30
|
-
status_code = colorize_status_code(res.status.to_s)
|
31
|
-
duration = calculate_duration(start_time, end_time)
|
32
|
-
|
33
|
-
log_message = "[#{start_time}] \"#{http_method} #{req.path_info}\" " \
|
34
|
-
"#{status_code} #{format('%.2f', duration)}ms"
|
35
|
-
|
36
|
-
::Kernel.puts(log_message)
|
37
|
-
end
|
38
|
-
|
39
|
-
private
|
40
|
-
|
41
|
-
# This method is used to colorize the request method.
|
42
|
-
#
|
43
|
-
# @parameter request_method [String] The request method
|
44
|
-
#
|
45
|
-
# @return [String] The colorized request method
|
46
|
-
#
|
47
|
-
# @private
|
48
|
-
#
|
49
|
-
def colorize_http_method(request_method)
|
50
|
-
case request_method
|
51
|
-
in 'GET' then 'GET'.green
|
52
|
-
in 'POST' then 'POST'.magenta
|
53
|
-
in 'PUT' then 'PUT'.yellow
|
54
|
-
in 'DELETE' then 'DELETE'.red
|
55
|
-
else request_method.blue
|
56
|
-
end
|
57
|
-
end
|
58
|
-
|
59
|
-
# This method is used to colorize the status code.
|
60
|
-
#
|
61
|
-
# @param status_code [String] The status code
|
62
|
-
#
|
63
|
-
# @return [String] The colorized status code
|
64
|
-
#
|
65
|
-
# @private
|
66
|
-
#
|
67
|
-
def colorize_status_code(status_code)
|
68
|
-
case status_code
|
69
|
-
in '2' then status_code.green
|
70
|
-
in '3' then status_code.blue
|
71
|
-
in '4' then status_code.yellow
|
72
|
-
in '5' then status_code.red
|
73
|
-
else status_code
|
74
|
-
end
|
75
|
-
end
|
76
|
-
|
77
|
-
# This method is used to calculate the duration.
|
78
|
-
#
|
79
|
-
# @param start_time [Time] The start time
|
80
|
-
#
|
81
|
-
# @param end_time [Time] The end time
|
82
|
-
# @return [Float] The duration
|
83
|
-
#
|
84
|
-
# @api private
|
85
|
-
#
|
86
|
-
def calculate_duration(start_time, end_time)
|
87
|
-
millis_in_second = 1000.0
|
88
|
-
(end_time - start_time) * millis_in_second
|
89
|
-
end
|
90
|
-
end
|
91
|
-
end
|
92
|
-
end
|
93
|
-
end
|
@@ -1,97 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
# Released under the MIT License.
|
4
|
-
# Copyright, 2023, by Aristóteles Coutinho.
|
5
|
-
|
6
|
-
require 'console'
|
7
|
-
|
8
|
-
# This middleware is used to reload files in development mode.
|
9
|
-
#
|
10
|
-
module Lenna
|
11
|
-
module Middleware
|
12
|
-
module Default
|
13
|
-
class Reload
|
14
|
-
attr_accessor :directories, :files_mtime
|
15
|
-
|
16
|
-
# Initializes a new instance of Middleware::Default::Reload.
|
17
|
-
#
|
18
|
-
# @parameter directories [Array] An array of directories to monitor.
|
19
|
-
#
|
20
|
-
# @return [Middleware::Default::Reload] A new instance of Middleware::Default::Reload.
|
21
|
-
def initialize(directories = [])
|
22
|
-
self.files_mtime = {}
|
23
|
-
self.directories = directories
|
24
|
-
|
25
|
-
monitor_directories(directories)
|
26
|
-
end
|
27
|
-
|
28
|
-
# Calls the middleware.
|
29
|
-
#
|
30
|
-
# @parameter req [Rack::Request] The request.
|
31
|
-
# @parameter _res [Rack::Response] The response.
|
32
|
-
# @parameter next_middleware [Proc] The next middleware.
|
33
|
-
#
|
34
|
-
# @return [void]
|
35
|
-
#
|
36
|
-
def call(_req, _res, next_middleware)
|
37
|
-
reload_if_needed
|
38
|
-
|
39
|
-
next_middleware.call
|
40
|
-
rescue ::StandardError => error
|
41
|
-
::Console.error(self, error)
|
42
|
-
end
|
43
|
-
|
44
|
-
private
|
45
|
-
|
46
|
-
# Reloads files if needed.
|
47
|
-
#
|
48
|
-
# @return [void]
|
49
|
-
#
|
50
|
-
def reload_if_needed
|
51
|
-
modified_files = check_for_modified_files
|
52
|
-
|
53
|
-
reload_files(modified_files) unless modified_files.empty?
|
54
|
-
end
|
55
|
-
|
56
|
-
# Monitors directories for changes.
|
57
|
-
#
|
58
|
-
# @parameter directories [Array] An array of directories to monitor.
|
59
|
-
#
|
60
|
-
# @return [void]
|
61
|
-
#
|
62
|
-
def monitor_directories(directories)
|
63
|
-
directories.each do |directory|
|
64
|
-
::Dir.glob(directory).each { |file| files_mtime[file] = ::File.mtime(file) }
|
65
|
-
end
|
66
|
-
end
|
67
|
-
|
68
|
-
# Checks for modified files.
|
69
|
-
#
|
70
|
-
# @return [Array] An array of modified files.
|
71
|
-
#
|
72
|
-
# @example
|
73
|
-
# check_for_modified_files #=> ["/path/to/file.rb"]
|
74
|
-
#
|
75
|
-
def check_for_modified_files
|
76
|
-
@files_mtime.select do |file, last_mtime|
|
77
|
-
::File.mtime(file) > last_mtime
|
78
|
-
end.keys
|
79
|
-
end
|
80
|
-
|
81
|
-
# Reloads files.
|
82
|
-
#
|
83
|
-
# @parameter files [Array] An array of files(paths) to reload.
|
84
|
-
#
|
85
|
-
# @return [void]
|
86
|
-
#
|
87
|
-
def reload_files(files)
|
88
|
-
files.each do |file|
|
89
|
-
::Console.debug("Reloading #{file}")
|
90
|
-
::Kernel.load file
|
91
|
-
@files_mtime[file] = ::File.mtime(file)
|
92
|
-
end
|
93
|
-
end
|
94
|
-
end
|
95
|
-
end
|
96
|
-
end
|
97
|
-
end
|
data/lib/lenna/router/builder.rb
DELETED
@@ -1,124 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
# Released under the MIT License.
|
4
|
-
# Copyright, 2023, by Aristóteles Coutinho.
|
5
|
-
|
6
|
-
module Lenna
|
7
|
-
class Router
|
8
|
-
# The Route::Builder class is responsible for building the tree of routes.
|
9
|
-
#
|
10
|
-
# The tree of routes is built by adding routes to the tree. Each route is
|
11
|
-
# represented by a node in the tree and each node has a path and an
|
12
|
-
# endpoint. The path is the path of the route and the endpoint is then
|
13
|
-
# action to be executed when the route is matched.
|
14
|
-
#
|
15
|
-
# Those nodes are stored in a cache to avoid rebuilding the tree of routes
|
16
|
-
# for each request.
|
17
|
-
#
|
18
|
-
# The tree use `Trie` data structures to optimize the search for a route.
|
19
|
-
# The trie is a tree where each node is a character of the path.
|
20
|
-
# This way, the search for a route is O(n) where n is the length of the
|
21
|
-
# path.
|
22
|
-
#
|
23
|
-
# @private
|
24
|
-
#
|
25
|
-
class Builder
|
26
|
-
def initialize(root_node) = @root_node = root_node
|
27
|
-
|
28
|
-
# This method will add a route to the tree of routes.
|
29
|
-
#
|
30
|
-
# @parameter method [String] the HTTP method
|
31
|
-
# @parameter path [String] the path to be matched
|
32
|
-
# @parameter action [Proc] the action to be executed
|
33
|
-
# @parameter cache [Cache] the cache to be used
|
34
|
-
#
|
35
|
-
# @return [void]
|
36
|
-
#
|
37
|
-
def call(method, path, action, cache)
|
38
|
-
path_key = cache.cache_key(method, path)
|
39
|
-
|
40
|
-
return if cache.exist?(path_key)
|
41
|
-
|
42
|
-
current_node = find_or_create_route_node(path)
|
43
|
-
setup_endpoint(current_node, method, action)
|
44
|
-
|
45
|
-
cache.add(path_key, current_node)
|
46
|
-
end
|
47
|
-
|
48
|
-
private
|
49
|
-
|
50
|
-
# This method will create routes that are missing.
|
51
|
-
# @parameter path [String] the path to be matched
|
52
|
-
#
|
53
|
-
# @return [Node] the node that matches the path
|
54
|
-
#
|
55
|
-
def find_or_create_route_node(path)
|
56
|
-
current_node = @root_node
|
57
|
-
split_path(path).each do |part|
|
58
|
-
current_node = find_or_create_node(current_node, part)
|
59
|
-
end
|
60
|
-
current_node
|
61
|
-
end
|
62
|
-
|
63
|
-
# This method will create the nodes that are missing.
|
64
|
-
#
|
65
|
-
# @parameter current_node [Node] the current node
|
66
|
-
# @parameter part [String] the part of the path
|
67
|
-
#
|
68
|
-
# @return [Node] the node that matches the part of the path
|
69
|
-
#
|
70
|
-
# This way, the tree of routes is built.
|
71
|
-
# @example Given the part ':id' and the tree bellow:
|
72
|
-
# root
|
73
|
-
# └── users
|
74
|
-
# └── :id
|
75
|
-
# The method will return the node :id.
|
76
|
-
# If the node :id does not exist, it will be created.
|
77
|
-
# The tree will be:
|
78
|
-
# root
|
79
|
-
# └── users
|
80
|
-
# └── :id
|
81
|
-
#
|
82
|
-
def find_or_create_node(current_node, part)
|
83
|
-
if part.start_with?(':')
|
84
|
-
# If it is a placeholder, then we just create or update
|
85
|
-
# the placeholder node with the placeholder name.
|
86
|
-
placeholder_name = part[1..].to_sym
|
87
|
-
current_node.children[:placeholder] ||= Node.new(
|
88
|
-
{},
|
89
|
-
nil,
|
90
|
-
placeholder_name
|
91
|
-
)
|
92
|
-
else
|
93
|
-
current_node.children[part] ||= Node.new
|
94
|
-
end
|
95
|
-
current_node.children[part.start_with?(':') ? :placeholder : part]
|
96
|
-
end
|
97
|
-
|
98
|
-
# This method will setup the endpoint of the current node.
|
99
|
-
#
|
100
|
-
# @parameter current_node [Node] the current node
|
101
|
-
# @parameter method [String] the HTTP method
|
102
|
-
# @parameter action [Proc] the action to be executed
|
103
|
-
#
|
104
|
-
# @return [void]
|
105
|
-
#
|
106
|
-
def setup_endpoint(current_node, method, action)
|
107
|
-
current_node.endpoint ||= {}
|
108
|
-
current_node.endpoint[method] = action
|
109
|
-
end
|
110
|
-
|
111
|
-
# This method will split the path into parts.
|
112
|
-
#
|
113
|
-
# @parameter path [String] the path to be split
|
114
|
-
#
|
115
|
-
# @return [Array] the splitted path
|
116
|
-
#
|
117
|
-
# TODO: Move this to a separate file and require it here.
|
118
|
-
# Maybe utils or something like that.
|
119
|
-
# Use Rack::Utils.split_path_info instead.
|
120
|
-
#
|
121
|
-
def split_path(path) = path.split('/').reject(&:empty?)
|
122
|
-
end
|
123
|
-
end
|
124
|
-
end
|
data/lib/lenna/router/cache.rb
DELETED
@@ -1,52 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
# Released under the MIT License.
|
4
|
-
# Copyright, 2023, by Aristóteles Coutinho.
|
5
|
-
|
6
|
-
module Lenna
|
7
|
-
class Router
|
8
|
-
# This class is used to cache the routes.
|
9
|
-
#
|
10
|
-
# @private
|
11
|
-
#
|
12
|
-
class Cache
|
13
|
-
def initialize = @cache = {}
|
14
|
-
|
15
|
-
# This method is used to generate a key for the cache.
|
16
|
-
#
|
17
|
-
# @parameter [String] method
|
18
|
-
# @parameter [String] path
|
19
|
-
#
|
20
|
-
# @return [String]
|
21
|
-
#
|
22
|
-
def cache_key(method, path) = "#{method} #{path}"
|
23
|
-
|
24
|
-
# This method is used to add a route to the cache.
|
25
|
-
#
|
26
|
-
# @parameter route_key [String] The key for the route.
|
27
|
-
# @parameter node [Lenna::Route::Node] The node for the route.
|
28
|
-
#
|
29
|
-
# @return [Lenna::Route::Node]
|
30
|
-
#
|
31
|
-
def add(route_key, node) = @cache[route_key] = node
|
32
|
-
|
33
|
-
# This method is used to get a route from the cache.
|
34
|
-
#
|
35
|
-
# @parameter route_key [String] The key for the route.
|
36
|
-
#
|
37
|
-
# @return [Lenna::Route::Node]
|
38
|
-
#
|
39
|
-
def get(route_key) = @cache[route_key]
|
40
|
-
|
41
|
-
# This method is used to check if a route is in the cache.
|
42
|
-
#
|
43
|
-
# @api public
|
44
|
-
#
|
45
|
-
# @parameter route_key [String] The key for the route.
|
46
|
-
#
|
47
|
-
# @return [Boolean]
|
48
|
-
#
|
49
|
-
def exist?(route_key) = @cache.key?(route_key)
|
50
|
-
end
|
51
|
-
end
|
52
|
-
end
|
@@ -1,77 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
# Released under the MIT License.
|
4
|
-
# Copyright, 2023, by Aristóteles Coutinho.
|
5
|
-
|
6
|
-
module Lenna
|
7
|
-
class Router
|
8
|
-
# This class is used to manage the namespaces.
|
9
|
-
#
|
10
|
-
# @private `Since v0.1.0`
|
11
|
-
#
|
12
|
-
class NamespaceStack
|
13
|
-
# @return [Array] The stack of namespaces
|
14
|
-
#
|
15
|
-
# @private
|
16
|
-
#
|
17
|
-
attr_reader :stack
|
18
|
-
|
19
|
-
# The initialize method is used to initialize the stack of namespaces.
|
20
|
-
#
|
21
|
-
# @private
|
22
|
-
#
|
23
|
-
# @attibute stack [Array] The stack of namespaces
|
24
|
-
#
|
25
|
-
# @return [void]
|
26
|
-
#
|
27
|
-
def initialize = @stack = ['']
|
28
|
-
|
29
|
-
# This method is used to push a prefix to the stack.
|
30
|
-
#
|
31
|
-
# @parameter prefix [String] The prefix to be pushed
|
32
|
-
#
|
33
|
-
# @return [void]
|
34
|
-
#
|
35
|
-
# ex.
|
36
|
-
#
|
37
|
-
# stack = NamespaceStack.new
|
38
|
-
# stack.push('/users')
|
39
|
-
# stack.current_prefix # => '/users'
|
40
|
-
#
|
41
|
-
# @see #resolve_prefix
|
42
|
-
#
|
43
|
-
def push(prefix)
|
44
|
-
@stack.push(resolve_prefix(prefix))
|
45
|
-
end
|
46
|
-
|
47
|
-
# This method is used to remove the last prefix from the stack.
|
48
|
-
#
|
49
|
-
# @return [String] The popped prefix
|
50
|
-
#
|
51
|
-
def pop
|
52
|
-
@stack.pop unless @stack.size == 1
|
53
|
-
end
|
54
|
-
|
55
|
-
# @return [String] The current prefix
|
56
|
-
#
|
57
|
-
def current_prefix = @stack.last
|
58
|
-
|
59
|
-
# The to_s method is used to return the current prefix.
|
60
|
-
#
|
61
|
-
# @return [String] The current prefix
|
62
|
-
#
|
63
|
-
def to_s = current_prefix
|
64
|
-
|
65
|
-
private
|
66
|
-
|
67
|
-
# The resolve_prefix method is used to resolve the prefix.
|
68
|
-
#
|
69
|
-
# @parameter prefix [String] The prefix to be resolved
|
70
|
-
# @return [String] The resolved prefix
|
71
|
-
#
|
72
|
-
def resolve_prefix(prefix)
|
73
|
-
current_prefix + prefix
|
74
|
-
end
|
75
|
-
end
|
76
|
-
end
|
77
|
-
end
|