lennarb 1.2.0 → 1.4.0
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/.devcontainer/Dockerfile +8 -0
- data/.devcontainer/devcontainer.json +20 -0
- data/.editorconfig +9 -0
- data/.github/workflows/coverage.yaml +58 -0
- data/.github/workflows/documentation.yaml +47 -0
- data/.github/workflows/main.yaml +27 -0
- data/.github/workflows/test.yaml +49 -0
- data/.gitignore +12 -0
- data/.standard.yml +23 -0
- data/.tool-versions +1 -0
- data/LICENCE +24 -0
- data/Rakefile +10 -0
- data/benchmark/memory.png +0 -0
- data/benchmark/rps.png +0 -0
- data/benchmark/runtime_with_startup.png +0 -0
- data/bin/console +8 -0
- data/bin/release +15 -0
- data/bin/setup +8 -0
- data/changelog.md +60 -7
- data/exe/lenna +2 -3
- data/gems.rb +29 -0
- data/guides/getting-started/readme.md +201 -0
- data/guides/links.yaml +6 -0
- data/guides/performance/readme.md +120 -0
- data/guides/response/readme.md +83 -0
- data/lennarb.gemspec +44 -0
- data/lib/lennarb/constansts.rb +1 -0
- data/lib/lennarb/request.rb +83 -41
- data/lib/lennarb/response.rb +131 -138
- data/lib/lennarb/route_node.rb +59 -82
- data/lib/lennarb/version.rb +2 -7
- data/lib/lennarb.rb +52 -124
- data/license.md +1 -2
- data/logo/lennarb.png +0 -0
- data/readme.md +18 -7
- metadata +103 -62
- data/lib/lennarb/application/base.rb +0 -283
- data/lib/lennarb/plugin.rb +0 -35
data/lib/lennarb/response.rb
CHANGED
@@ -1,140 +1,133 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
# Released under the MIT License.
|
4
|
-
# Copyright, 2023-2024, by Aristóteles Coutinho.
|
5
|
-
|
6
1
|
class Lennarb
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
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
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
end
|
139
|
-
end
|
2
|
+
class Response
|
3
|
+
# @!attribute [rw] status
|
4
|
+
# @returns [Integer]
|
5
|
+
#
|
6
|
+
attr_accessor :status
|
7
|
+
|
8
|
+
# @!attribute [r] body
|
9
|
+
# @returns [Array]
|
10
|
+
#
|
11
|
+
attr_reader :body
|
12
|
+
|
13
|
+
# @!attribute [r] headers
|
14
|
+
# @returns [Hash]
|
15
|
+
#
|
16
|
+
attr_reader :headers
|
17
|
+
|
18
|
+
# @!attribute [r] length
|
19
|
+
# @returns [Integer]
|
20
|
+
#
|
21
|
+
attr_reader :length
|
22
|
+
|
23
|
+
# Constants
|
24
|
+
#
|
25
|
+
LOCATION = "location"
|
26
|
+
private_constant :LOCATION
|
27
|
+
|
28
|
+
CONTENT_TYPE = "content-type"
|
29
|
+
private_constant :CONTENT_TYPE
|
30
|
+
|
31
|
+
CONTENT_LENGTH = "content-length"
|
32
|
+
private_constant :CONTENT_LENGTH
|
33
|
+
|
34
|
+
ContentType = {HTML: "text/html", TEXT: "text/plain", JSON: "application/json"}.freeze
|
35
|
+
# Initialize the response object
|
36
|
+
#
|
37
|
+
# @returns [Response]
|
38
|
+
#
|
39
|
+
def initialize
|
40
|
+
@status = 404
|
41
|
+
@headers = {}
|
42
|
+
@body = []
|
43
|
+
@length = 0
|
44
|
+
end
|
45
|
+
|
46
|
+
# Set the response header
|
47
|
+
#
|
48
|
+
# @parameter [String] key
|
49
|
+
#
|
50
|
+
# @returns [String] value
|
51
|
+
#
|
52
|
+
def [](key)
|
53
|
+
@headers[key]
|
54
|
+
end
|
55
|
+
|
56
|
+
# Get the response header
|
57
|
+
#
|
58
|
+
# @parameter [String] key
|
59
|
+
# @parameter [String] value
|
60
|
+
#
|
61
|
+
# @returns [String] value
|
62
|
+
#
|
63
|
+
def []=(key, value)
|
64
|
+
@headers[key] = value
|
65
|
+
end
|
66
|
+
|
67
|
+
# Write to the response body
|
68
|
+
#
|
69
|
+
# @parameter [String] str
|
70
|
+
#
|
71
|
+
# @returns [String] str
|
72
|
+
#
|
73
|
+
def write(str)
|
74
|
+
str = str.to_s
|
75
|
+
@length += str.bytesize
|
76
|
+
@headers[CONTENT_LENGTH] = @length.to_s
|
77
|
+
@body << str
|
78
|
+
end
|
79
|
+
|
80
|
+
# Set the response type to text
|
81
|
+
#
|
82
|
+
# @parameter [String] str
|
83
|
+
#
|
84
|
+
# @returns [String] str
|
85
|
+
#
|
86
|
+
def text(str)
|
87
|
+
@headers[CONTENT_TYPE] = ContentType[:TEXT]
|
88
|
+
write(str)
|
89
|
+
end
|
90
|
+
|
91
|
+
# Set the response type to html
|
92
|
+
#
|
93
|
+
# @parameter [String] str
|
94
|
+
#
|
95
|
+
# @returns [String] str
|
96
|
+
#
|
97
|
+
def html(str)
|
98
|
+
@headers[CONTENT_TYPE] = ContentType[:HTML]
|
99
|
+
write(str)
|
100
|
+
end
|
101
|
+
|
102
|
+
# Set the response type to json
|
103
|
+
#
|
104
|
+
# @parameter [String] str
|
105
|
+
#
|
106
|
+
# @returns [String] str
|
107
|
+
#
|
108
|
+
def json(str)
|
109
|
+
@headers[CONTENT_TYPE] = ContentType[:JSON]
|
110
|
+
write(str)
|
111
|
+
end
|
112
|
+
|
113
|
+
# Redirect the response
|
114
|
+
#
|
115
|
+
# @parameter [String] path
|
116
|
+
# @parameter [Integer] status, default: 302
|
117
|
+
#
|
118
|
+
def redirect(path, status = 302)
|
119
|
+
@headers[LOCATION] = path
|
120
|
+
@status = status
|
121
|
+
|
122
|
+
throw :halt, finish
|
123
|
+
end
|
124
|
+
|
125
|
+
# Finish the response
|
126
|
+
#
|
127
|
+
# @returns [Array] response
|
128
|
+
#
|
129
|
+
def finish
|
130
|
+
[@status, @headers, @body]
|
131
|
+
end
|
132
|
+
end
|
140
133
|
end
|
data/lib/lennarb/route_node.rb
CHANGED
@@ -1,84 +1,61 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
# Released under the MIT License.
|
4
|
-
# Copyright, 2023-2024, by Aristóteles Coutinho.
|
5
|
-
|
6
1
|
class Lennarb
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
end
|
67
|
-
end
|
68
|
-
|
69
|
-
[nil, nil]
|
70
|
-
end
|
71
|
-
|
72
|
-
# Merge the other RouteNode into the current one
|
73
|
-
#
|
74
|
-
# @parameter other [RouteNode] The other RouteNode to merge into the current one
|
75
|
-
#
|
76
|
-
# @return [void]
|
77
|
-
#
|
78
|
-
def merge!(other)
|
79
|
-
self.static_children.merge!(other.static_children)
|
80
|
-
self.dynamic_children.merge!(other.dynamic_children)
|
81
|
-
self.blocks.merge!(other.blocks)
|
82
|
-
end
|
83
|
-
end
|
2
|
+
class RouteNode
|
3
|
+
attr_accessor :static_children, :dynamic_children, :blocks, :param_key
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@blocks = {}
|
7
|
+
@param_key = nil
|
8
|
+
@static_children = {}
|
9
|
+
@dynamic_children = {}
|
10
|
+
end
|
11
|
+
|
12
|
+
def add_route(parts, http_method, block)
|
13
|
+
current_node = self
|
14
|
+
|
15
|
+
parts.each do |part|
|
16
|
+
if part.start_with?(":")
|
17
|
+
param_sym = part[1..].to_sym
|
18
|
+
current_node.dynamic_children[param_sym] ||= RouteNode.new
|
19
|
+
dynamic_node = current_node.dynamic_children[param_sym]
|
20
|
+
dynamic_node.param_key = param_sym
|
21
|
+
current_node = dynamic_node
|
22
|
+
else
|
23
|
+
current_node.static_children[part] ||= RouteNode.new
|
24
|
+
current_node = current_node.static_children[part]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
current_node.blocks[http_method] = block
|
29
|
+
end
|
30
|
+
|
31
|
+
def match_route(parts, http_method, params: {})
|
32
|
+
if parts.empty?
|
33
|
+
return [blocks[http_method], params] if blocks[http_method]
|
34
|
+
else
|
35
|
+
part = parts.first
|
36
|
+
rest = parts[1..]
|
37
|
+
|
38
|
+
if static_children.key?(part)
|
39
|
+
result_block, result_params = static_children[part].match_route(rest, http_method, params:)
|
40
|
+
return [result_block, result_params] if result_block
|
41
|
+
end
|
42
|
+
|
43
|
+
dynamic_children.each_value do |dyn_node|
|
44
|
+
new_params = params.dup
|
45
|
+
new_params[dyn_node.param_key] = part
|
46
|
+
result_block, result_params = dyn_node.match_route(rest, http_method, params: new_params)
|
47
|
+
|
48
|
+
return [result_block, result_params] if result_block
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
[nil, nil]
|
53
|
+
end
|
54
|
+
|
55
|
+
def merge!(other)
|
56
|
+
static_children.merge!(other.static_children)
|
57
|
+
dynamic_children.merge!(other.dynamic_children)
|
58
|
+
blocks.merge!(other.blocks)
|
59
|
+
end
|
60
|
+
end
|
84
61
|
end
|
data/lib/lennarb/version.rb
CHANGED
data/lib/lennarb.rb
CHANGED
@@ -1,144 +1,72 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
# Released under the MIT License.
|
4
|
-
# Copyright, 2023-2024, by Aristóteles Coutinho.
|
5
|
-
|
6
1
|
# Core extensions
|
7
2
|
#
|
8
|
-
require
|
9
|
-
require
|
3
|
+
require "pathname"
|
4
|
+
require "rack"
|
5
|
+
require "bundler"
|
10
6
|
|
11
|
-
|
12
|
-
|
13
|
-
require_relative
|
14
|
-
require_relative
|
15
|
-
require_relative
|
16
|
-
require_relative 'lennarb/response'
|
17
|
-
require_relative 'lennarb/route_node'
|
18
|
-
require_relative 'lennarb/version'
|
7
|
+
require_relative "lennarb/request"
|
8
|
+
require_relative "lennarb/response"
|
9
|
+
require_relative "lennarb/route_node"
|
10
|
+
require_relative "lennarb/version"
|
11
|
+
require_relative "lennarb/constansts"
|
19
12
|
|
20
13
|
class Lennarb
|
21
|
-
|
22
|
-
#
|
23
|
-
class LennarbError < StandardError; end
|
24
|
-
|
25
|
-
# @attribute [r] root
|
26
|
-
# @returns [RouteNode]
|
27
|
-
#
|
28
|
-
attr_reader :_root
|
14
|
+
class LennarbError < StandardError; end
|
29
15
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
# Initialize the application
|
36
|
-
#
|
37
|
-
# @yield { ... } The application
|
38
|
-
#
|
39
|
-
# @returns [Lennarb]
|
40
|
-
#
|
41
|
-
def initialize
|
42
|
-
@_root = RouteNode.new
|
43
|
-
@_applied_plugins = []
|
44
|
-
yield self if block_given?
|
45
|
-
end
|
16
|
+
def initialize
|
17
|
+
@_mutex ||= Mutex.new
|
18
|
+
yield self if block_given?
|
19
|
+
end
|
46
20
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
#
|
53
|
-
SplitPath = ->(path) { path.split('/').reject(&:empty?) }
|
54
|
-
private_constant :SplitPath
|
21
|
+
HTTP_METHODS.each do |http_method|
|
22
|
+
define_method(http_method.downcase) do |path, &block|
|
23
|
+
add_route(path, http_method, block)
|
24
|
+
end
|
25
|
+
end
|
55
26
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
#
|
60
|
-
# @returns [Array] response
|
61
|
-
#
|
62
|
-
def call(env)
|
63
|
-
http_method = env[Rack::REQUEST_METHOD].to_sym
|
64
|
-
parts = SplitPath[env[Rack::PATH_INFO]]
|
27
|
+
def root
|
28
|
+
@root ||= RouteNode.new
|
29
|
+
end
|
65
30
|
|
66
|
-
|
67
|
-
|
31
|
+
def app
|
32
|
+
@app ||= begin
|
33
|
+
request_handler = ->(env) { process_request(env) }
|
68
34
|
|
69
|
-
|
70
|
-
|
35
|
+
Rack::Builder.app do
|
36
|
+
run request_handler
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
71
40
|
|
72
|
-
|
73
|
-
|
74
|
-
@res.finish
|
75
|
-
end
|
76
|
-
end
|
41
|
+
def initializer!
|
42
|
+
Bundler.require(:default, ENV["LENNA_ENV"] || "development")
|
77
43
|
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
#
|
82
|
-
def freeze! = @_root.freeze
|
44
|
+
root.freeze
|
45
|
+
app.freeze
|
46
|
+
end
|
83
47
|
|
84
|
-
|
85
|
-
#
|
86
|
-
# @parameter [String] path
|
87
|
-
# @parameter [Proc] block
|
88
|
-
#
|
89
|
-
# @returns [void]
|
90
|
-
#
|
91
|
-
def get(path, &block) = add_route(path, :GET, block)
|
92
|
-
def put(path, &block) = add_route(path, :PUT, block)
|
93
|
-
def post(path, &block) = add_route(path, :POST, block)
|
94
|
-
def head(path, &block) = add_route(path, :HEAD, block)
|
95
|
-
def patch(path, &block) = add_route(path, :PATCH, block)
|
96
|
-
def delete(path, &block) = add_route(path, :DELETE, block)
|
97
|
-
def options(path, &block) = add_route(path, :OPTIONS, block)
|
48
|
+
def call(env) = @_mutex.synchronize { app.call(env) }
|
98
49
|
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
# @parameter [Block] block
|
104
|
-
#
|
105
|
-
# @returns [void]
|
106
|
-
#
|
107
|
-
def plugin(plugin_name, *, &)
|
108
|
-
return if @_applied_plugins.include?(plugin_name)
|
50
|
+
def add_route(path, http_method, block)
|
51
|
+
parts = path.split("/").reject(&:empty?)
|
52
|
+
root.add_route(parts, http_method, block)
|
53
|
+
end
|
109
54
|
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
plugin_module.setup(self.class, *, &) if plugin_module.respond_to?(:setup)
|
55
|
+
private def process_request(env)
|
56
|
+
http_method = env[Rack::REQUEST_METHOD].to_sym
|
57
|
+
parts = env[Rack::PATH_INFO].split("/").reject(&:empty?)
|
114
58
|
|
115
|
-
|
116
|
-
|
59
|
+
block, params = root.match_route(parts, http_method)
|
60
|
+
return [404, {"content-type" => Response::ContentType[:TEXT]}, ["Not Found"]] unless block
|
117
61
|
|
118
|
-
|
119
|
-
|
120
|
-
# @parameter other [RouteNode] The other RouteNode to merge into the current one
|
121
|
-
#
|
122
|
-
# @return [void]
|
123
|
-
#
|
124
|
-
def merge!(other)
|
125
|
-
raise "Expected a Lennarb instance, got #{other.class}" unless other.is_a?(Lennarb)
|
62
|
+
res = Response.new
|
63
|
+
req = Request.new(env, params)
|
126
64
|
|
127
|
-
|
65
|
+
catch(:halt) do
|
66
|
+
instance_exec(req, res, &block)
|
67
|
+
res.finish
|
68
|
+
end
|
69
|
+
rescue => e
|
70
|
+
[500, {"content-type" => Response::ContentType[:TEXT]}, ["Internal Server Error - #{e.message}"]]
|
128
71
|
end
|
129
|
-
|
130
|
-
private
|
131
|
-
|
132
|
-
# Add a route
|
133
|
-
#
|
134
|
-
# @parameter [String] path
|
135
|
-
# @parameter [String] http_method
|
136
|
-
# @parameter [Proc] block
|
137
|
-
#
|
138
|
-
# @returns [void]
|
139
|
-
#
|
140
|
-
def add_route(path, http_method, block)
|
141
|
-
parts = SplitPath[path]
|
142
|
-
@_root.add_route(parts, http_method, block)
|
143
|
-
end
|
144
72
|
end
|
data/license.md
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
# MIT License
|
2
2
|
|
3
|
-
Copyright
|
4
|
-
Copyright, 2023, by aristotelesbr.
|
3
|
+
Copyright (c) 2023-2025 Aristótels Coutinho
|
5
4
|
|
6
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
7
6
|
of this software and associated documentation files (the "Software"), to deal
|
data/logo/lennarb.png
ADDED
Binary file
|