lennarb 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 4c397642a693a43af01eac2929b1a43c4f6258f43b63f4dadb6c6bd2245eb54f
4
+ data.tar.gz: eb65ddcc1896bd51e7f254e22698f8bc9d190c388e9dd892e35d6a348e9d5724
5
+ SHA512:
6
+ metadata.gz: d36a2a07a847997c2b55cfb0c47aa894cd6d19ef1214721b3c4d1c8922e93077f0b47e5989b63a6f0fb555f5cee95a4b7938a232c7ddea56e11401a95074e971
7
+ data.tar.gz: 8827458b308d68e874326b143670112af07e7b03f7945bfdd1db7f3d04c1e22daf196f59bf5c46e0ed741add8b52dc11d7447d545f869620b5efbfb4f50c5efe
data/CHANGELOG.md ADDED
@@ -0,0 +1,29 @@
1
+ # Changelog
2
+ All notable changes to this project will be documented in this file.
3
+
4
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [Unreleased]
8
+
9
+ ### Added
10
+ - New feature for the next release.
11
+
12
+ ### Added
13
+ - Initial release of the project.
14
+
15
+ ### Changed
16
+ - Example change to an existing feature.
17
+
18
+ ### Deprecated
19
+ - Example feature that will be removed in future releases.
20
+
21
+ ### Removed
22
+ - Example feature that was removed.
23
+
24
+ ### Fixed
25
+ - Example bug fix.
26
+
27
+ ### Security
28
+ - Example security fix.
29
+
data/LICENCE ADDED
@@ -0,0 +1,24 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2023, Aristóteles Coutinho costa
4
+
5
+ Permission is hereby granted, free of charge, to any person
6
+ obtaining a copy of this software and associated documentation
7
+ files (the "Software"), to deal in the Software without
8
+ restriction, including without limitation the rights to use,
9
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the
11
+ Software is furnished to do so, subject to the following
12
+ conditions:
13
+
14
+ The above copyright notice and this permission notice shall be
15
+ included in all copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
19
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
21
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
22
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
24
+ OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,31 @@
1
+ # Lennarb
2
+
3
+ TODO: Delete this and the text below, and describe your gem
4
+
5
+ Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/lennarb`. To experiment with that code, run `bin/console` for an interactive prompt.
6
+
7
+ ## Installation
8
+
9
+ TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_PRIOR_TO_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
10
+
11
+ Install the gem and add to the application's Gemfile by executing:
12
+
13
+ $ bundle add UPDATE_WITH_YOUR_GEM_NAME_PRIOR_TO_RELEASE_TO_RUBYGEMS_ORG
14
+
15
+ If bundler is not being used to manage dependencies, install the gem by executing:
16
+
17
+ $ gem install UPDATE_WITH_YOUR_GEM_NAME_PRIOR_TO_RELEASE_TO_RUBYGEMS_ORG
18
+
19
+ ## Usage
20
+
21
+ TODO: Write usage instructions here
22
+
23
+ ## Development
24
+
25
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
26
+
27
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
28
+
29
+ ## Contributing
30
+
31
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/lennarb.
data/lib/lenna/base.rb ADDED
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ # External dependencies
4
+ require 'puma'
5
+
6
+ # Internal dependencies
7
+ require 'lenna/middleware/default/error_handler'
8
+ require 'lenna/middleware/default/logging'
9
+ require 'lenna/router'
10
+
11
+ module Lenna
12
+ # The base class is used to start the server.
13
+ class Base < Router
14
+ DEFAULT_PORT = 3000
15
+ private_constant :DEFAULT_PORT
16
+ DEFAULT_HOST = 'localhost'
17
+ private_constant :DEFAULT_HOST
18
+
19
+ # This method will start the server.
20
+ #
21
+ # @param port [Integer] The port to listen on (default: 3000)
22
+ # @param host [String] The host to listen on (default: '
23
+ # @return [void]
24
+ #
25
+ # @example
26
+ # app = Lenna::Base.new
27
+ # app.listen(8080)
28
+ # # => ⚡ Listening on localhost:8080
29
+ #
30
+ # or specify the host and port
31
+ #
32
+ # app = Lenna::Base.new
33
+ # app.listen(8000, host: '0.0.0.0')
34
+ # # => ⚡ Listening on 0.0.0.0:8000
35
+ #
36
+ # @api public
37
+ #
38
+ # @todo: Add Lenna::Server to handle the server logic
39
+ #
40
+ # @since 0.1.0
41
+ def listen(port = DEFAULT_PORT, host: DEFAULT_HOST, **)
42
+ puts "⚡ Listening on #{host}:#{port}"
43
+
44
+ # Add the logging middleware to the stack
45
+ use(Middleware::Default::Logging, Middleware::Default::ErrorHandler)
46
+
47
+ server = ::Puma::Server.new(self, **)
48
+ server.add_tcp_listener(host, port)
49
+ server.run.join
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lenna
4
+ module Middleware
5
+ # The MiddlewareManager class is responsible for managing the middlewares.
6
+ #
7
+ # @attr mutex [Mutex] the mutex used to synchronize the
8
+ # access to the global middlewares and
9
+ # the middleware chains cache.
10
+ # @attr global_middlewares [Array] the global middlewares
11
+ # @attr middleware_chains_cache [Hash] the middleware chains cache
12
+ #
13
+ # @note Middleware chains are cached by action.
14
+ # The middlewares that are added to a specific route are added to the
15
+ # global middlewares.
16
+ #
17
+ # @api private
18
+ #
19
+ # @since 0.1.0
20
+ class App
21
+ # @return [Mutex] the mutex used to synchronize the access to the global
22
+ attr_reader :global_middlewares
23
+
24
+ # @return [Hash] the middleware chains cache
25
+ attr_reader :middleware_chains_cache
26
+
27
+ # This method will initialize the global middlewares and the
28
+ # middleware chains cache.
29
+ #
30
+ # @return [void]
31
+ #
32
+ # @since 0.1.0
33
+ def initialize
34
+ @mutex = ::Mutex.new
35
+ @global_middlewares = []
36
+ @middleware_chains_cache = {}
37
+ end
38
+
39
+ # This method is used to add a middleware to the global middlewares.
40
+ # @param middlewares [Array] the middlewares to be used
41
+ # @return [void]
42
+ #
43
+ # @since 0.1.0
44
+ def use(middlewares)
45
+ @mutex.synchronize do
46
+ @global_middlewares += Array(middlewares)
47
+ @middleware_chains_cache = {}
48
+ end
49
+ end
50
+
51
+ # This method is used to fetch or build the middleware chain for the given
52
+ # action and route middlewares.
53
+ #
54
+ # @param action [Proc] the action to be executed
55
+ # @param route_middlewares [Array] the middlewares to be used
56
+ # @return [Proc] the middleware chain
57
+ #
58
+ # @see #build_middleware_chain
59
+ #
60
+ # @since 0.1.0
61
+ def fetch_or_build_middleware_chain(action, route_middlewares)
62
+ middleware_signature = action.object_id.to_s
63
+
64
+ @mutex.synchronize do
65
+ @middleware_chains_cache[middleware_signature] ||=
66
+ build_middleware_chain(action, route_middlewares)
67
+ end
68
+ end
69
+
70
+ # This method is used to build the middleware chain for the given action
71
+ # and middlewares.
72
+ #
73
+ # @param action [Proc] the action to be executed
74
+ # @param middlewares [Array] the middlewares to be used
75
+ # @return [Proc] the middleware chain
76
+ #
77
+ # @since 0.1.0
78
+ #
79
+ # @example Given the action:
80
+ # `->(req, res) { res << 'Hello' }` and the
81
+ # middlewares [mw1, mw2], the middleware
82
+ # chain will be:
83
+ # mw1 -> mw2 -> action
84
+ # The action will be the last middleware in the
85
+ # chain.
86
+ def build_middleware_chain(action, middlewares)
87
+ all_middlewares = @global_middlewares + Array(middlewares)
88
+
89
+ all_middlewares.reverse.reduce(action) do |next_middleware, middleware|
90
+ ->(req, res) {
91
+ middleware.call(
92
+ req,
93
+ res,
94
+ -> {
95
+ next_middleware.call(req, res)
96
+ }
97
+ )
98
+ }
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,205 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cgi'
4
+
5
+ module Lenna
6
+ module Middleware
7
+ module Default
8
+ # This middleware will handle errors.
9
+ module ErrorHandler
10
+ extend self
11
+
12
+ # This method will be called by the server.
13
+ #
14
+ # @param req [Rack::Request] The request object
15
+ # @param res [Rack::Response] The response object
16
+ # @param next_middleware [Proc] The next middleware in the stack
17
+ # @return [void]
18
+ #
19
+ # @api private
20
+ #
21
+ def call(req, res, next_middleware)
22
+ next_middleware.call
23
+ rescue StandardError => e
24
+ env = req.env
25
+ log_error(env, e)
26
+
27
+ render_error_page(e, env, res)
28
+ end
29
+
30
+ private
31
+
32
+ # This method will render the error page.
33
+ #
34
+ # @param error [StandardError] The error object
35
+ # @param env [Hash] The environment variables
36
+ # @param res [Rack::Response] The response object
37
+ # @return [void]
38
+ #
39
+ # @api private
40
+ def render_error_page(error, env, res)
41
+ res.put_status(500)
42
+ res.put_header('Content-Type', 'text/html')
43
+ res.put_body(error_page(error, env))
44
+ end
45
+
46
+ # This method will log the error.
47
+ #
48
+ # @param env [Hash] The environment variables
49
+ # @param error [StandardError] The error object
50
+ # @return [void]
51
+ #
52
+ # @api private
53
+ def log_error(env, error)
54
+ env['rack.errors'].puts error.message
55
+ env['rack.errors'].puts error.backtrace.join("\n")
56
+ env['rack.errors'].flush
57
+ end
58
+
59
+ # This method will render the error page.
60
+ def error_page(error, env)
61
+ style = <<-STYLE
62
+ <style>
63
+ body { font-family: 'Helvetica Neue', sans-serif; background-color: #F7F7F7; color: #333; margin: 0; padding: 0; }
64
+ .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; }
65
+ h1 { color: #c0392b }
66
+ h2 { padding: 0; margin: 0; font-size: 1.2em; }
67
+ .error-details, .backtrace { text-align: left; }
68
+ .error-details strong { color: #e74c3c; }
69
+ pre { background: #ecf0f1; padding: 15px; overflow: auto; border-left: 5px solid #e74c3c; font-size: 0.9em; }
70
+ .backtrace { margin-top: 20px; }
71
+ .backtrace pre { border-color: #3498db; }
72
+ svg { fill: #e74c3c; width: 50px; height: auto; }
73
+ .container { display: flex; justify-content: space-between; align-items: center; align-content: center;}
74
+ </style>
75
+ STYLE
76
+
77
+ # SVG logo
78
+ svg_logo = <<~SVG
79
+ <svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="100" height="100" viewBox="0 0 100 100">
80
+ <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>
81
+ </svg>
82
+ #{' '}
83
+ SVG
84
+
85
+ # HTML page
86
+ <<-HTML
87
+ <!DOCTYPE html>
88
+ <html lang="en">
89
+ <head>
90
+ <meta charset="UTF-8">
91
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
92
+ #{' '}
93
+ <title>System Error</title>
94
+ #{style}
95
+ </head>
96
+ <body>
97
+ <div class="error-container">
98
+ <div class="container">
99
+ <div class="item-left">
100
+ <h1>Oops! An error has occurred.</h1>
101
+ </div>
102
+ <div class="item-right">#{svg_logo}</div>
103
+ </div>
104
+ #{' '}
105
+ #{error_message_and_backtrace(error, env)}
106
+ </div>
107
+ </body>
108
+ </html>
109
+ HTML
110
+ end
111
+
112
+ # This method will render the error message and backtrace.
113
+ #
114
+ # @param error [StandardError] The error object
115
+ # @param env [Hash] The environment variables
116
+ # @return [String] The HTML string
117
+ #
118
+ # @api private
119
+ def error_message_and_backtrace(error, env)
120
+ if env['RACK_ENV'] == 'development'
121
+ truncated_message =
122
+ error.message[0..500] + (error.message.length > 500 ? '...' : '')
123
+
124
+ file, line = error.backtrace.first.split(':')
125
+ line_number = Integer(line)
126
+ <<-DETAILS
127
+ <div class="error-details">
128
+ <h2>Error Details:</h2>
129
+ <p>
130
+ <strong>Message:</strong> <span id="error-message">#{CGI.escapeHTML(truncated_message)}</span>
131
+ </p>
132
+ <div>
133
+ <p><strong>Location:</strong> #{CGI.escapeHTML(file)}:#{line_number}</p>
134
+ <pre>#{extract_source(file, line_number)}</pre>
135
+ </div>
136
+
137
+ <details>
138
+ <summary>Details</summary>
139
+ <p id="full-message" style="display: none;">
140
+ <h3>Full Backtrace:</h3>
141
+ <p><strong>Full Message:</strong> #{CGI.escapeHTML(error.message)}</p>
142
+ <pre>#{error.backtrace.join("\n")}</pre>
143
+ </p>
144
+ </details>
145
+ </div>
146
+ DETAILS
147
+ else
148
+ "<p>We're sorry, but something went wrong. We've been notified " \
149
+ 'about this issue and will take a look at it shortly.</p>'
150
+ end
151
+ end
152
+
153
+ # This method will extract the source code.
154
+ #
155
+ # @param file [String] The file path
156
+ # @param line_number [Integer] The line number
157
+ # @return [String] The HTML string
158
+ #
159
+ # @api private
160
+ #
161
+ # @example:
162
+ # extract_source('/path/to/file.rb', 10)
163
+ # # => "<strong style='color: red;'> 7: </strong> =>
164
+ # def foo\n<strong style='color: red;'> 8: </strong>
165
+ # puts 'bar'\n<strong style='color: red;'> 9: </strong>
166
+ # end\n<strong style='color: red;'> 10: </strong> foo\n<strong
167
+ # style='color: red;'> 11: </strong> "
168
+ def extract_source(file, line_number)
169
+ lines = ::File.readlines(file)
170
+ start_line = [line_number - 3, 0].max
171
+ end_line = [line_number + 3, lines.size].min
172
+
173
+ line_ranger = lines[start_line...end_line]
174
+
175
+ format_lines(line_ranger, line_number).join
176
+ end
177
+
178
+ # This method will format the lines.
179
+ #
180
+ # @api private
181
+ #
182
+ # @example:
183
+ # format_lines(line_ranger, line_number)
184
+ # # => ["<strong style='color: red;'> 7: </strong> =>\n",
185
+ # "<strong style='color: red;'> 8: </strong> puts 'bar'\n",
186
+ # "<strong style='color: red;'> 9: </strong> end\n",
187
+ # "<strong style='color: red;'> 10: </strong> foo\n",
188
+ # "<strong style='color: red;'> 11: </strong> "]
189
+ def format_lines(lines, highlight_line)
190
+ lines.map.with_index(highlight_line - 3 + 1) do |line, line_num|
191
+ line_number_text = "#{line_num.to_s.rjust(6)}: "
192
+ formatted_line = ::CGI.escapeHTML(line)
193
+
194
+ if line_num == highlight_line
195
+ "<strong style='color: red;'>#{line_number_text}</strong> " \
196
+ "#{formatted_line}"
197
+ else
198
+ "#{line_number_text}#{formatted_line}"
199
+ end
200
+ end
201
+ end
202
+ end
203
+ end
204
+ end
205
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'colorize'
4
+
5
+ module Lenna
6
+ module Middleware
7
+ module Default
8
+ # The Logging module is responsible for logging the requests.
9
+ #
10
+ # @api private
11
+ #
12
+ # @example:
13
+ # Logging.call(req, res, next_middleware)
14
+ # # => [2021-01-01 00:00:00 +0000] "GET /" 200 0.00ms
15
+ module Logging
16
+ extend self
17
+
18
+ # This method is used to log the request.
19
+ #
20
+ # @param req [Rack::Request ] The request
21
+ # @param res [Rack::Response] The response
22
+ # @param next_middleware [Proc] The next middleware
23
+ def call(req, res, next_middleware)
24
+ start_time = ::Time.now
25
+ next_middleware.call
26
+ end_time = ::Time.now
27
+
28
+ http_method = colorize_http_method(req.request_method)
29
+ status_code = colorize_status_code(res.status.to_s)
30
+ duration = calculate_duration(start_time, end_time)
31
+
32
+ log_message = "[#{start_time}] \"#{http_method} #{req.path_info}\" " \
33
+ "#{status_code} #{format('%.2f', duration)}ms"
34
+
35
+ ::Kernel.puts(log_message)
36
+ end
37
+
38
+ private
39
+
40
+ # This method is used to colorize the request method.
41
+ #
42
+ # @param request_method [String] The request method
43
+ # @return [String] The colorized request method
44
+ #
45
+ # @api private
46
+ #
47
+ # @example:
48
+ # colorize_http_method('GET') # => 'GET'.green
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
+ # @return [String] The colorized status code
63
+ #
64
+ # @api private
65
+ #
66
+ # @example:
67
+ # colorize_status_code('200') # => '200'.green
68
+ def colorize_status_code(status_code)
69
+ case status_code[0]
70
+ in '2' then status_code.green
71
+ in '3' then status_code.blue
72
+ in '4' then status_code.yellow
73
+ in '5' then status_code.red
74
+ else status_code
75
+ end
76
+ end
77
+
78
+ # This method is used to calculate the duration.
79
+ #
80
+ # @param start_time [Time] The start time
81
+ # @param end_time [Time] The end time
82
+ # @return [Float] The duration
83
+ #
84
+ # @api private
85
+ #
86
+ # @example:
87
+ # calculate_duration(Time.now, Time.now + 1) # => 1000
88
+ def calculate_duration(start_time, end_time)
89
+ millis_in_second = 1000.0
90
+ (end_time - start_time) * millis_in_second
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lenna
4
+ class Router
5
+ # The Route::Builder class is responsible for building the tree of routes.
6
+ #
7
+ # The tree of routes is built by adding routes to the tree. Each route is
8
+ # represented by a node in the tree and each node has a path and an
9
+ # endpoint. The path is the path of the route and the endpoint is then
10
+ # action to be executed when the route is matched.
11
+ #
12
+ # Those nodes are stored in a cache to avoid rebuilding the tree of routes
13
+ # for each request.
14
+ #
15
+ # The tree use `Trie` data structures to optimize the search for a route.
16
+ # The trie is a tree where each node is a character of the path.
17
+ # This way, the search for a route is O(n) where n is the length of the
18
+ # path.
19
+ #
20
+ class Builder
21
+ def initialize(root_node) = @root_node = root_node
22
+
23
+ # @param method [String] the HTTP method
24
+ # @param path [String] the path to be matched
25
+ # @param action [Proc] the action to be executed
26
+ # @param cache [Cache] the cache to be used
27
+ # @return [void]
28
+ def call(method, path, action, cache)
29
+ path_key = cache.cache_key(method, path)
30
+
31
+ return if cache.exist?(path_key)
32
+
33
+ current_node = find_or_create_route_node(path)
34
+ setup_endpoint(current_node, method, action)
35
+
36
+ cache.add(path_key, current_node)
37
+ end
38
+
39
+ private
40
+
41
+ # This method will create routes that are missing.
42
+ # @param path [String] the path to be matched
43
+ # @return [Node] the node that matches the path
44
+ def find_or_create_route_node(path)
45
+ current_node = @root_node
46
+ split_path(path).each do |part|
47
+ current_node = find_or_create_node(current_node, part)
48
+ end
49
+ current_node
50
+ end
51
+
52
+ # @param current_node [Node] the current node
53
+ # @param part [String] the part of the path
54
+ # @return [Node] the node that matches the part of the path
55
+ # @note This method will create the nodes that are missing.
56
+ # This way, the tree of routes is built.
57
+ # @example Given the part ':id' and the tree bellow:
58
+ # root
59
+ # └── users
60
+ # └── :id
61
+ # The method will return the node :id.
62
+ # If the node :id does not exist, it will be created.
63
+ # The tree will be:
64
+ # root
65
+ # └── users
66
+ # └── :id
67
+ def find_or_create_node(current_node, part)
68
+ if part.start_with?(':')
69
+ # If it is a placeholder, then we just create or update
70
+ # the placeholder node with the placeholder name.
71
+ placeholder_name = part[1..].to_sym
72
+ current_node.children[:placeholder] ||= Node.new(
73
+ {},
74
+ nil,
75
+ placeholder_name
76
+ )
77
+ else
78
+ current_node.children[part] ||= Node.new
79
+ end
80
+ current_node.children[part.start_with?(':') ? :placeholder : part]
81
+ end
82
+
83
+ # @param current_node [Node] the current node
84
+ # @param method [String] the HTTP method
85
+ # @param action [Proc] the action to be executed
86
+ def setup_endpoint(current_node, method, action)
87
+ current_node.endpoint ||= {}
88
+ current_node.endpoint[method] = action
89
+ end
90
+
91
+ # @param path [String] the path to be split
92
+ # @return [Array] the splitted path
93
+ # @todo: Move this to a separate file and require it here.
94
+ # Maybe utils or something like that.
95
+ # Use Rack::Utils.split_path_info instead.
96
+ def split_path(path) = path.split('/').reject(&:empty?)
97
+ end
98
+ end
99
+ end