lennarb 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +29 -0
- data/LICENCE +24 -0
- data/README.md +31 -0
- data/lib/lenna/base.rb +52 -0
- data/lib/lenna/middleware/app.rb +103 -0
- data/lib/lenna/middleware/default/error_handler.rb +205 -0
- data/lib/lenna/middleware/default/logging.rb +95 -0
- data/lib/lenna/router/builder.rb +99 -0
- data/lib/lenna/router/cache.rb +38 -0
- data/lib/lenna/router/namespace_stack.rb +73 -0
- data/lib/lenna/router/request.rb +77 -0
- data/lib/lenna/router/response.rb +357 -0
- data/lib/lenna/router/route_matcher.rb +69 -0
- data/lib/lenna/router.rb +173 -0
- data/lib/lennarb.rb +3 -0
- metadata +172 -0
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
|