lennarb 0.1.5 → 0.1.7
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 +210 -0
- data/exe/lenna +17 -0
- data/lib/lenna/application.rb +53 -0
- data/lib/lenna/cli/app.rb +39 -0
- data/lib/lenna/cli/commands/create_project.rb +125 -0
- data/lib/lenna/cli/commands/interface.rb +20 -0
- data/lib/lenna/cli/commands/start_server.rb +143 -0
- data/lib/lenna/cli/templates/application.erb +11 -0
- data/lib/lenna/cli/templates/config.ru.erb +5 -0
- data/lib/lenna/cli/templates/gemfile.erb +14 -0
- data/lib/lenna/middleware/app.rb +107 -92
- data/lib/lenna/middleware/default/error_handler.rb +184 -179
- data/lib/lenna/middleware/default/logging.rb +79 -81
- data/lib/lenna/middleware/default/reload.rb +97 -0
- data/lib/lenna/router/builder.rb +111 -86
- data/lib/lenna/router/cache.rb +44 -30
- data/lib/lenna/router/namespace_stack.rb +66 -62
- data/lib/lenna/router/request.rb +125 -101
- data/lib/lenna/router/response.rb +505 -375
- data/lib/lenna/router/route_matcher.rb +56 -57
- data/lib/lenna/router.rb +187 -154
- data/lib/lennarb/array_extensions.rb +25 -11
- data/lib/lennarb/version.rb +5 -2
- data/lib/lennarb.rb +39 -1
- data/license.md +21 -0
- data/readme.md +31 -0
- metadata +99 -41
- data/CHANGELOG.md +0 -62
- data/LICENCE +0 -24
- data/README.md +0 -229
- data/lib/lenna/base.rb +0 -52
@@ -1,95 +1,93 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
# Released under the MIT License.
|
4
|
+
# Copyright, 2023, by Aristóteles Coutinho.
|
5
|
+
|
3
6
|
require 'colorize'
|
4
7
|
|
5
8
|
module Lenna
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
# # => [2021-01-01 00:00:00 +0000] "GET /" 200 0.00ms
|
15
|
-
module Logging
|
16
|
-
extend self
|
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
17
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
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
|
27
28
|
|
28
|
-
|
29
|
-
|
30
|
-
|
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)
|
31
32
|
|
32
|
-
|
33
|
-
|
33
|
+
log_message = "[#{start_time}] \"#{http_method} #{req.path_info}\" " \
|
34
|
+
"#{status_code} #{format('%.2f', duration)}ms"
|
34
35
|
|
35
|
-
|
36
|
-
|
36
|
+
::Kernel.puts(log_message)
|
37
|
+
end
|
37
38
|
|
38
|
-
|
39
|
+
private
|
39
40
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
end
|
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
58
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
end
|
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
|
77
76
|
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
end
|
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
|
95
93
|
end
|
@@ -0,0 +1,97 @@
|
|
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
CHANGED
@@ -1,99 +1,124 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
# Released under the MIT License.
|
4
|
+
# Copyright, 2023, by Aristóteles Coutinho.
|
5
|
+
|
3
6
|
module Lenna
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
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
|
22
27
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
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)
|
30
39
|
|
31
|
-
|
40
|
+
return if cache.exist?(path_key)
|
32
41
|
|
33
|
-
|
34
|
-
|
42
|
+
current_node = find_or_create_route_node(path)
|
43
|
+
setup_endpoint(current_node, method, action)
|
35
44
|
|
36
|
-
|
37
|
-
|
45
|
+
cache.add(path_key, current_node)
|
46
|
+
end
|
38
47
|
|
39
|
-
|
48
|
+
private
|
40
49
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
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
|
51
62
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
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
|
82
97
|
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
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
|
90
110
|
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
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
|
99
124
|
end
|
data/lib/lenna/router/cache.rb
CHANGED
@@ -1,38 +1,52 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
# Released under the MIT License.
|
4
|
+
# Copyright, 2023, by Aristóteles Coutinho.
|
5
|
+
|
3
6
|
module Lenna
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
7
|
+
class Router
|
8
|
+
# This class is used to cache the routes.
|
9
|
+
#
|
10
|
+
# @private
|
11
|
+
#
|
12
|
+
class Cache
|
13
|
+
def initialize = @cache = {}
|
9
14
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
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}"
|
16
23
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
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
|
23
32
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
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]
|
29
40
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
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
|
38
52
|
end
|