lennarb 0.1.5 → 0.1.7
Sign up to get free protection for your applications and to get access to all the features.
- 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
|