lennarb 1.1.0 → 1.3.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/changelog.md +20 -0
- data/lib/lennarb/plugin.rb +45 -31
- data/lib/lennarb/plugins/hooks.rb +117 -0
- data/lib/lennarb/plugins/mount.rb +66 -0
- data/lib/lennarb/request.rb +76 -37
- data/lib/lennarb/response.rb +133 -133
- data/lib/lennarb/route_node.rb +50 -56
- data/lib/lennarb/version.rb +2 -2
- data/lib/lennarb.rb +140 -114
- data/license.md +1 -2
- metadata +5 -4
- data/lib/lennarb/application/base.rb +0 -253
data/lib/lennarb/response.rb
CHANGED
@@ -4,137 +4,137 @@
|
|
4
4
|
# Copyright, 2023-2024, by Aristóteles Coutinho.
|
5
5
|
|
6
6
|
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
|
-
|
139
|
-
|
7
|
+
class Response
|
8
|
+
# @!attribute [rw] status
|
9
|
+
# @returns [Integer]
|
10
|
+
#
|
11
|
+
attr_accessor :status
|
12
|
+
|
13
|
+
# @!attribute [r] body
|
14
|
+
# @returns [Array]
|
15
|
+
#
|
16
|
+
attr_reader :body
|
17
|
+
|
18
|
+
# @!attribute [r] headers
|
19
|
+
# @returns [Hash]
|
20
|
+
#
|
21
|
+
attr_reader :headers
|
22
|
+
|
23
|
+
# @!attribute [r] length
|
24
|
+
# @returns [Integer]
|
25
|
+
#
|
26
|
+
attr_reader :length
|
27
|
+
|
28
|
+
# Constants
|
29
|
+
#
|
30
|
+
LOCATION = 'location'
|
31
|
+
private_constant :LOCATION
|
32
|
+
|
33
|
+
CONTENT_TYPE = 'content-type'
|
34
|
+
private_constant :CONTENT_TYPE
|
35
|
+
|
36
|
+
CONTENT_LENGTH = 'content-length'
|
37
|
+
private_constant :CONTENT_LENGTH
|
38
|
+
|
39
|
+
ContentType = { HTML: 'text/html', TEXT: 'text/plain', JSON: 'application/json' }.freeze
|
40
|
+
private_constant :ContentType
|
41
|
+
|
42
|
+
# Initialize the response object
|
43
|
+
#
|
44
|
+
# @returns [Response]
|
45
|
+
#
|
46
|
+
def initialize
|
47
|
+
@status = 404
|
48
|
+
@headers = {}
|
49
|
+
@body = []
|
50
|
+
@length = 0
|
51
|
+
end
|
52
|
+
|
53
|
+
# Set the response header
|
54
|
+
#
|
55
|
+
# @parameter [String] key
|
56
|
+
#
|
57
|
+
# @returns [String] value
|
58
|
+
#
|
59
|
+
def [](key)
|
60
|
+
@headers[key]
|
61
|
+
end
|
62
|
+
|
63
|
+
# Get the response header
|
64
|
+
#
|
65
|
+
# @parameter [String] key
|
66
|
+
# @parameter [String] value
|
67
|
+
#
|
68
|
+
# @returns [String] value
|
69
|
+
#
|
70
|
+
def []=(key, value)
|
71
|
+
@headers[key] = value
|
72
|
+
end
|
73
|
+
|
74
|
+
# Write to the response body
|
75
|
+
#
|
76
|
+
# @parameter [String] str
|
77
|
+
#
|
78
|
+
# @returns [String] str
|
79
|
+
#
|
80
|
+
def write(str)
|
81
|
+
str = str.to_s
|
82
|
+
@length += str.bytesize
|
83
|
+
@headers[CONTENT_LENGTH] = @length.to_s
|
84
|
+
@body << str
|
85
|
+
end
|
86
|
+
|
87
|
+
# Set the response type to text
|
88
|
+
#
|
89
|
+
# @parameter [String] str
|
90
|
+
#
|
91
|
+
# @returns [String] str
|
92
|
+
#
|
93
|
+
def text(str)
|
94
|
+
@headers[CONTENT_TYPE] = ContentType[:TEXT]
|
95
|
+
write(str)
|
96
|
+
end
|
97
|
+
|
98
|
+
# Set the response type to html
|
99
|
+
#
|
100
|
+
# @parameter [String] str
|
101
|
+
#
|
102
|
+
# @returns [String] str
|
103
|
+
#
|
104
|
+
def html(str)
|
105
|
+
@headers[CONTENT_TYPE] = ContentType[:HTML]
|
106
|
+
write(str)
|
107
|
+
end
|
108
|
+
|
109
|
+
# Set the response type to json
|
110
|
+
#
|
111
|
+
# @parameter [String] str
|
112
|
+
#
|
113
|
+
# @returns [String] str
|
114
|
+
#
|
115
|
+
def json(str)
|
116
|
+
@headers[CONTENT_TYPE] = ContentType[:JSON]
|
117
|
+
write(str)
|
118
|
+
end
|
119
|
+
|
120
|
+
# Redirect the response
|
121
|
+
#
|
122
|
+
# @parameter [String] path
|
123
|
+
# @parameter [Integer] status, default: 302
|
124
|
+
#
|
125
|
+
def redirect(path, status = 302)
|
126
|
+
@headers[LOCATION] = path
|
127
|
+
@status = status
|
128
|
+
|
129
|
+
throw :halt, finish
|
130
|
+
end
|
131
|
+
|
132
|
+
# Finish the response
|
133
|
+
#
|
134
|
+
# @returns [Array] response
|
135
|
+
#
|
136
|
+
def finish
|
137
|
+
[@status, @headers, @body]
|
138
|
+
end
|
139
|
+
end
|
140
140
|
end
|
data/lib/lennarb/route_node.rb
CHANGED
@@ -4,69 +4,63 @@
|
|
4
4
|
# Copyright, 2023-2024, by Aristóteles Coutinho.
|
5
5
|
|
6
6
|
class Lennarb
|
7
|
-
|
8
|
-
|
7
|
+
class RouteNode
|
8
|
+
attr_accessor :static_children, :dynamic_children, :blocks, :param_key
|
9
9
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
@param_key = nil
|
17
|
-
@static_children = {}
|
18
|
-
@dynamic_children = {}
|
19
|
-
end
|
10
|
+
def initialize
|
11
|
+
@blocks = {}
|
12
|
+
@param_key = nil
|
13
|
+
@static_children = {}
|
14
|
+
@dynamic_children = {}
|
15
|
+
end
|
20
16
|
|
21
|
-
|
22
|
-
|
23
|
-
# @parameter parts [Array<String>] The parts of the route
|
24
|
-
# @parameter http_method [Symbol] The HTTP method of the route
|
25
|
-
# @parameter block [Proc] The block to be executed when the route is matched
|
26
|
-
#
|
27
|
-
# @return [void]
|
28
|
-
#
|
29
|
-
def add_route(parts, http_method, block)
|
30
|
-
current_node = self
|
17
|
+
def add_route(parts, http_method, block)
|
18
|
+
current_node = self
|
31
19
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
20
|
+
parts.each do |part|
|
21
|
+
if part.start_with?(':')
|
22
|
+
param_sym = part[1..].to_sym
|
23
|
+
current_node.dynamic_children[param_sym] ||= RouteNode.new
|
24
|
+
dynamic_node = current_node.dynamic_children[param_sym]
|
25
|
+
dynamic_node.param_key = param_sym
|
26
|
+
current_node = dynamic_node
|
27
|
+
else
|
28
|
+
current_node.static_children[part] ||= RouteNode.new
|
29
|
+
current_node = current_node.static_children[part]
|
30
|
+
end
|
31
|
+
end
|
44
32
|
|
45
|
-
|
46
|
-
|
33
|
+
current_node.blocks[http_method] = block
|
34
|
+
end
|
47
35
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
36
|
+
def match_route(parts, http_method, params: {})
|
37
|
+
if parts.empty?
|
38
|
+
return [blocks[http_method], params] if blocks[http_method]
|
39
|
+
else
|
40
|
+
part = parts.first
|
41
|
+
rest = parts[1..]
|
54
42
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
43
|
+
if static_children.key?(part)
|
44
|
+
result_block, result_params = static_children[part].match_route(rest, http_method, params:)
|
45
|
+
return [result_block, result_params] if result_block
|
46
|
+
end
|
59
47
|
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
48
|
+
dynamic_children.each_value do |dyn_node|
|
49
|
+
new_params = params.dup
|
50
|
+
new_params[dyn_node.param_key] = part
|
51
|
+
result_block, result_params = dyn_node.match_route(rest, http_method, params: new_params)
|
64
52
|
|
65
|
-
|
66
|
-
|
67
|
-
|
53
|
+
return [result_block, result_params] if result_block
|
54
|
+
end
|
55
|
+
end
|
68
56
|
|
69
|
-
|
70
|
-
|
71
|
-
|
57
|
+
[nil, nil]
|
58
|
+
end
|
59
|
+
|
60
|
+
def merge!(other)
|
61
|
+
static_children.merge!(other.static_children)
|
62
|
+
dynamic_children.merge!(other.dynamic_children)
|
63
|
+
blocks.merge!(other.blocks)
|
64
|
+
end
|
65
|
+
end
|
72
66
|
end
|
data/lib/lennarb/version.rb
CHANGED
data/lib/lennarb.rb
CHANGED
@@ -8,9 +8,6 @@
|
|
8
8
|
require 'pathname'
|
9
9
|
require 'rack'
|
10
10
|
|
11
|
-
# Base class for Lennarb
|
12
|
-
#
|
13
|
-
require_relative 'lennarb/application/base'
|
14
11
|
require_relative 'lennarb/plugin'
|
15
12
|
require_relative 'lennarb/request'
|
16
13
|
require_relative 'lennarb/response'
|
@@ -18,115 +15,144 @@ require_relative 'lennarb/route_node'
|
|
18
15
|
require_relative 'lennarb/version'
|
19
16
|
|
20
17
|
class Lennarb
|
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
|
-
|
18
|
+
class LennarbError < StandardError; end
|
19
|
+
|
20
|
+
attr_reader :_root, :_plugins, :_loaded_plugins, :_middlewares, :_app
|
21
|
+
|
22
|
+
def self.use(middleware, *args, &block)
|
23
|
+
@_middlewares ||= []
|
24
|
+
@_middlewares << [middleware, args, block]
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.get(path, &block) = add_route(path, :GET, block)
|
28
|
+
def self.put(path, &block) = add_route(path, :PUT, block)
|
29
|
+
def self.post(path, &block) = add_route(path, :POST, block)
|
30
|
+
def self.head(path, &block) = add_route(path, :HEAD, block)
|
31
|
+
def self.patch(path, &block) = add_route(path, :PATCH, block)
|
32
|
+
def self.delete(path, &block) = add_route(path, :DELETE, block)
|
33
|
+
def self.options(path, &block) = add_route(path, :OPTIONS, block)
|
34
|
+
|
35
|
+
def self.inherited(subclass)
|
36
|
+
super
|
37
|
+
subclass.instance_variable_set(:@_root, RouteNode.new)
|
38
|
+
subclass.instance_variable_set(:@_plugins, [])
|
39
|
+
subclass.instance_variable_set(:@_middlewares, @_middlewares&.dup || [])
|
40
|
+
|
41
|
+
Plugin.load_defaults! if Plugin.load_defaults?
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.plugin(plugin_name, *, &)
|
45
|
+
@_loaded_plugins ||= {}
|
46
|
+
@_plugins ||= []
|
47
|
+
|
48
|
+
return if @_loaded_plugins.key?(plugin_name)
|
49
|
+
|
50
|
+
plugin_module = Plugin.load(plugin_name)
|
51
|
+
plugin_module.configure(self, *, &) if plugin_module.respond_to?(:configure)
|
52
|
+
|
53
|
+
@_loaded_plugins[plugin_name] = plugin_module
|
54
|
+
@_plugins << plugin_name
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.freeze!
|
58
|
+
app = new
|
59
|
+
app.freeze!
|
60
|
+
app
|
61
|
+
end
|
62
|
+
|
63
|
+
def self.add_route(path, http_method, block)
|
64
|
+
@_root ||= RouteNode.new
|
65
|
+
parts = path.split('/').reject(&:empty?)
|
66
|
+
@_root.add_route(parts, http_method, block)
|
67
|
+
end
|
68
|
+
|
69
|
+
private_class_method :add_route
|
70
|
+
|
71
|
+
def initialize
|
72
|
+
@_mutex = Mutex.new
|
73
|
+
@_root = self.class.instance_variable_get(:@_root)&.dup || RouteNode.new
|
74
|
+
@_plugins = self.class.instance_variable_get(:@_plugins)&.dup || []
|
75
|
+
@_loaded_plugins = self.class.instance_variable_get(:@_loaded_plugins)&.dup || {}
|
76
|
+
@_middlewares = self.class.instance_variable_get(:@_middlewares)&.dup || []
|
77
|
+
|
78
|
+
build_app
|
79
|
+
|
80
|
+
yield self if block_given?
|
81
|
+
end
|
82
|
+
|
83
|
+
def call(env) = @_mutex.synchronize { @_app.call(env) }
|
84
|
+
|
85
|
+
def freeze!
|
86
|
+
return self if @_mounted
|
87
|
+
|
88
|
+
@_root.freeze
|
89
|
+
@_plugins.freeze
|
90
|
+
@_loaded_plugins.freeze
|
91
|
+
@_middlewares.freeze
|
92
|
+
@_app.freeze if @_app.respond_to?(:freeze)
|
93
|
+
self
|
94
|
+
end
|
95
|
+
|
96
|
+
def get(path, &block) = add_route(path, :GET, block)
|
97
|
+
def put(path, &block) = add_route(path, :PUT, block)
|
98
|
+
def post(path, &block) = add_route(path, :POST, block)
|
99
|
+
def head(path, &block) = add_route(path, :HEAD, block)
|
100
|
+
def patch(path, &block) = add_route(path, :PATCH, block)
|
101
|
+
def delete(path, &block) = add_route(path, :DELETE, block)
|
102
|
+
def options(path, &block) = add_route(path, :OPTIONS, block)
|
103
|
+
|
104
|
+
def plugin(plugin_name, *, &)
|
105
|
+
return if @_loaded_plugins.key?(plugin_name)
|
106
|
+
|
107
|
+
plugin_module = Plugin.load(plugin_name)
|
108
|
+
self.class.extend plugin_module::ClassMethods if plugin_module.const_defined?(:ClassMethods)
|
109
|
+
self.class.include plugin_module::InstanceMethods if plugin_module.const_defined?(:InstanceMethods)
|
110
|
+
plugin_module.configure(self, *, &) if plugin_module.respond_to?(:configure)
|
111
|
+
@_loaded_plugins[plugin_name] = plugin_module
|
112
|
+
@_plugins << plugin_name
|
113
|
+
end
|
114
|
+
|
115
|
+
private
|
116
|
+
|
117
|
+
def build_app
|
118
|
+
@_app = method(:process_request)
|
119
|
+
|
120
|
+
@_middlewares.reverse_each do |middleware, args, block|
|
121
|
+
@_app = middleware.new(@_app, *args, &block)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def process_request(env)
|
126
|
+
http_method = env[Rack::REQUEST_METHOD].to_sym
|
127
|
+
parts = env[Rack::PATH_INFO].split('/').reject(&:empty?)
|
128
|
+
|
129
|
+
block, params = @_root.match_route(parts, http_method)
|
130
|
+
return not_found unless block
|
131
|
+
|
132
|
+
res = Response.new
|
133
|
+
req = Request.new(env, params)
|
134
|
+
|
135
|
+
catch(:halt) do
|
136
|
+
instance_exec(req, res, &block)
|
137
|
+
res.finish
|
138
|
+
end
|
139
|
+
rescue StandardError => e
|
140
|
+
handle_error(e)
|
141
|
+
end
|
142
|
+
|
143
|
+
def handle_error(error)
|
144
|
+
case error
|
145
|
+
when ArgumentError
|
146
|
+
[400, { 'content-type' => 'text/plain' }, ["Bad Request: #{error.message}"]]
|
147
|
+
else
|
148
|
+
[500, { 'content-type' => 'text/plain' }, ["Internal Server Error: #{error.message}"]]
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
def not_found = [404, { 'content-type' => 'text/plain' }, ['Not Found']]
|
153
|
+
|
154
|
+
def add_route(path, http_method, block)
|
155
|
+
parts = path.split('/').reject(&:empty?)
|
156
|
+
@_root.add_route(parts, http_method, block)
|
157
|
+
end
|
132
158
|
end
|