lennarb 0.1.6 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/changelog.md +76 -10
- data/exe/lenna +15 -0
- 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 +87 -5
- data/license.md +2 -1
- data/readme.md +30 -13
- metadata +55 -20
- data/lib/lenna/application.rb +0 -53
- 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/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 -205
- data/lib/lennarb/array_extensions.rb +0 -31
data/lib/lenna/application.rb
DELETED
@@ -1,53 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
# Released under the MIT License.
|
4
|
-
# Copyright, 2023, by Aristóteles Coutinho.
|
5
|
-
|
6
|
-
# Internal dependencies
|
7
|
-
#
|
8
|
-
require 'lenna/middleware/default/error_handler'
|
9
|
-
require 'lenna/middleware/default/logging'
|
10
|
-
require 'lenna/router'
|
11
|
-
|
12
|
-
# The Lenna module is used to namespace the framework.
|
13
|
-
#
|
14
|
-
# @public
|
15
|
-
#
|
16
|
-
module Lenna
|
17
|
-
# The base class is used to start the server.
|
18
|
-
#
|
19
|
-
# @public
|
20
|
-
#
|
21
|
-
class Application < Router
|
22
|
-
# Initialize the base class
|
23
|
-
#
|
24
|
-
# @yield { ... } the block to be evaluated in the context of the instance.
|
25
|
-
#
|
26
|
-
# @return [void | Application] Returns the instance if a block is given.
|
27
|
-
#
|
28
|
-
def initialize
|
29
|
-
super
|
30
|
-
yield self if block_given?
|
31
|
-
end
|
32
|
-
end
|
33
|
-
|
34
|
-
# The base module is used to include the base class.
|
35
|
-
#
|
36
|
-
# @public
|
37
|
-
#
|
38
|
-
module Base
|
39
|
-
def self.included(base)
|
40
|
-
base.extend(ClassMethods)
|
41
|
-
end
|
42
|
-
|
43
|
-
module ClassMethods
|
44
|
-
# Initialize the base module
|
45
|
-
#
|
46
|
-
# @return [Lenna::Application] Returns the instance.
|
47
|
-
#
|
48
|
-
# @public
|
49
|
-
#
|
50
|
-
def app = @app ||= Lenna::Application.new
|
51
|
-
end
|
52
|
-
end
|
53
|
-
end
|
data/lib/lenna/middleware/app.rb
DELETED
@@ -1,118 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
# Released under the MIT License.
|
4
|
-
# Copyright, 2023, by Aristóteles Coutinho.
|
5
|
-
|
6
|
-
require 'singleton'
|
7
|
-
|
8
|
-
module Lenna
|
9
|
-
module Middleware
|
10
|
-
# The MiddlewareManager class is responsible for managing the middlewares.
|
11
|
-
#
|
12
|
-
# @attr global_middlewares [Array] the global middlewares
|
13
|
-
# @attr middleware_chains_cache [Hash] the middleware chains cache
|
14
|
-
#
|
15
|
-
# Middleware chains are cached by action.
|
16
|
-
# The middlewares that are added to a specific route are added to the
|
17
|
-
# global middlewares.
|
18
|
-
#
|
19
|
-
# @private Since `v0.1.0`
|
20
|
-
#
|
21
|
-
class App
|
22
|
-
include Singleton
|
23
|
-
|
24
|
-
# @return [Array] the global middlewares.
|
25
|
-
#
|
26
|
-
attr_accessor :global_middlewares
|
27
|
-
# @return [Hash] the middleware chains cache.
|
28
|
-
#
|
29
|
-
attr_accessor :middleware_chains_cache
|
30
|
-
|
31
|
-
# This method will initialize the global middlewares and the
|
32
|
-
# middleware chains cache.
|
33
|
-
#
|
34
|
-
# @return [void]
|
35
|
-
#
|
36
|
-
def initialize
|
37
|
-
@global_middlewares = []
|
38
|
-
@middleware_chains_cache = {}
|
39
|
-
end
|
40
|
-
|
41
|
-
# This method is used to reset the global middlewares and the middleware
|
42
|
-
# chains cache.
|
43
|
-
#
|
44
|
-
# @return [void]
|
45
|
-
#
|
46
|
-
def reset!
|
47
|
-
@global_middlewares = []
|
48
|
-
@middleware_chains_cache = {}
|
49
|
-
end
|
50
|
-
|
51
|
-
# This method is used to add a middleware to the global middlewares.
|
52
|
-
# @parameter middlewares [Array] the middlewares to be used
|
53
|
-
# @return [void]
|
54
|
-
#
|
55
|
-
def use(middlewares)
|
56
|
-
@global_middlewares += Array(middlewares)
|
57
|
-
@middleware_chains_cache = {}
|
58
|
-
end
|
59
|
-
|
60
|
-
# This method is used to fetch or build the middleware chain for the given
|
61
|
-
# action and route middlewares.
|
62
|
-
#
|
63
|
-
# @parameter action [Proc] the action to be executed
|
64
|
-
# @parameter route_middlewares [Array] the middlewares to be used
|
65
|
-
# @return [Proc] the middleware chain
|
66
|
-
#
|
67
|
-
#
|
68
|
-
def fetch_or_build_middleware_chain(
|
69
|
-
action,
|
70
|
-
route_middlewares,
|
71
|
-
http_method: nil,
|
72
|
-
path: nil
|
73
|
-
)
|
74
|
-
signature =
|
75
|
-
if http_method && path
|
76
|
-
[http_method, path, route_middlewares].hash.to_s
|
77
|
-
else
|
78
|
-
['global', route_middlewares].hash.to_s
|
79
|
-
end
|
80
|
-
|
81
|
-
@middleware_chains_cache[signature] ||=
|
82
|
-
build_middleware_chain(action, route_middlewares)
|
83
|
-
end
|
84
|
-
|
85
|
-
# This method is used to build the middleware chain for the given action
|
86
|
-
# and middlewares.
|
87
|
-
#
|
88
|
-
# @parameter action [Proc] the action to be executed
|
89
|
-
# @parameter middlewares [Array] the middlewares to be used
|
90
|
-
# @return [Proc] the middleware chain
|
91
|
-
#
|
92
|
-
# ex.
|
93
|
-
# Given the action:
|
94
|
-
# `->(req, res) { res << 'Hello' }` and the
|
95
|
-
# middlewares [mw1, mw2], the middleware
|
96
|
-
# chain will be:
|
97
|
-
# mw1 -> mw2 -> action
|
98
|
-
# The action will be the last middleware in the
|
99
|
-
# chain.
|
100
|
-
#
|
101
|
-
def build_middleware_chain(action, middlewares)
|
102
|
-
all_middlewares = (@global_middlewares + Array(middlewares))
|
103
|
-
|
104
|
-
all_middlewares.reverse.reduce(action) do |next_middleware, middleware|
|
105
|
-
->(req, res) {
|
106
|
-
middleware.call(
|
107
|
-
req,
|
108
|
-
res,
|
109
|
-
-> {
|
110
|
-
next_middleware.call(req, res)
|
111
|
-
}
|
112
|
-
)
|
113
|
-
}
|
114
|
-
end
|
115
|
-
end
|
116
|
-
end
|
117
|
-
end
|
118
|
-
end
|
@@ -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
|
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
|