lennarb 1.2.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 +49 -67
- data/lib/lennarb/version.rb +2 -2
- data/lib/lennarb.rb +138 -124
- data/license.md +1 -2
- metadata +5 -4
- data/lib/lennarb/application/base.rb +0 -283
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0fafe6e0a4a4fcb12d0cb7d75187b89c5ff2a8f4847bdab4db666d670fd1d40e
|
4
|
+
data.tar.gz: 43739df71271e6410f691b19d0e31c64ea5e883ae79150a3d0055d17a200fe8a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8015dceb914f8bf52ee77eff7faf3adca26cdb020a3acba0ce3cc58ecd7eec338e84afe87fcd691867e087a646875b06a9f278c220b25f4f627bc87ff7288306
|
7
|
+
data.tar.gz: c9a7b74731ac3fe4e607396647931656628e1df03968069891d65189b0f387df44d89054191a34a41bdaf7a3381ad6c2486f91fba13dfd4b5a3b04a79f299af7
|
data/changelog.md
CHANGED
@@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
7
7
|
|
8
8
|
## [Unreleased]
|
9
9
|
|
10
|
+
## [1.3.0] - 2024-11-21
|
11
|
+
|
12
|
+
### Added
|
13
|
+
|
14
|
+
- Add `Lennarb::Plugin` module to manage the plugins in the project. Now, the `Lennarb` class is the main class of the project.
|
15
|
+
|
16
|
+
- Automatically loads plugins from the default directory
|
17
|
+
|
18
|
+
- Supports custom plugin directories via `LENNARB_PLUGINS_PATH`
|
19
|
+
|
20
|
+
- Configurable through environment variables
|
21
|
+
|
22
|
+
### Changed
|
23
|
+
|
24
|
+
- Change the `finish` method from `Lennarb` class to call `halt(@res.finish)` method to finish the response.
|
25
|
+
|
26
|
+
### Removed
|
27
|
+
|
28
|
+
- Remove `Lennarb::ApplicationBase` class from the project. Now, the `Lennarb` class is the main class of the project.
|
29
|
+
|
10
30
|
## [0.6.1] - 2024-05-17
|
11
31
|
|
12
32
|
### Added
|
data/lib/lennarb/plugin.rb
CHANGED
@@ -1,35 +1,49 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
# Released under the MIT License.
|
4
|
-
# Copyright, 2023-2024, by Aristóteles Coutinho.
|
5
|
-
|
6
3
|
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
|
-
|
4
|
+
module Plugin
|
5
|
+
class Error < StandardError; end
|
6
|
+
|
7
|
+
@registry = {}
|
8
|
+
@defaults_loaded = false
|
9
|
+
|
10
|
+
class << self
|
11
|
+
attr_reader :registry
|
12
|
+
|
13
|
+
def register(name, mod)
|
14
|
+
registry[name.to_sym] = mod
|
15
|
+
end
|
16
|
+
|
17
|
+
def load(name)
|
18
|
+
registry[name.to_sym] || raise(Error, "Plugin #{name} not found")
|
19
|
+
end
|
20
|
+
|
21
|
+
def load_defaults!
|
22
|
+
return if @defaults_loaded
|
23
|
+
|
24
|
+
# 1. Register default plugins
|
25
|
+
plugins_path = File.expand_path('plugins', __dir__)
|
26
|
+
load_plugins_from_directory(plugins_path)
|
27
|
+
|
28
|
+
# # 2. Register custom plugins
|
29
|
+
ENV.fetch('LENNARB_PLUGINS_PATH', nil)&.split(File::PATH_SEPARATOR)&.each do |path|
|
30
|
+
load_plugins_from_directory(path)
|
31
|
+
end
|
32
|
+
|
33
|
+
@defaults_loaded = true
|
34
|
+
end
|
35
|
+
|
36
|
+
def load_defaults?
|
37
|
+
ENV.fetch('LENNARB_AUTO_LOAD_DEFAULTS', 'true') == 'true'
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def load_plugins_from_directory(path)
|
43
|
+
raise Error, "Plugin directory '#{path}' does not exist" unless File.directory?(path)
|
44
|
+
|
45
|
+
Dir["#{path}/**/*.rb"].each { require _1 }
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
35
49
|
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Lennarb
|
4
|
+
module Plugins
|
5
|
+
module Hooks
|
6
|
+
def self.configure(app)
|
7
|
+
app.instance_variable_set(:@_before_hooks, {})
|
8
|
+
app.instance_variable_set(:@_after_hooks, {})
|
9
|
+
app.extend(ClassMethods)
|
10
|
+
app.include(InstanceMethods)
|
11
|
+
end
|
12
|
+
|
13
|
+
module ClassMethods
|
14
|
+
def before(paths = '*', &block)
|
15
|
+
paths = Array(paths)
|
16
|
+
@_before_hooks ||= {}
|
17
|
+
|
18
|
+
paths.each do |path|
|
19
|
+
@_before_hooks[path] ||= []
|
20
|
+
@_before_hooks[path] << block
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def after(paths = '*', &block)
|
25
|
+
paths = Array(paths)
|
26
|
+
@_after_hooks ||= {}
|
27
|
+
|
28
|
+
paths.each do |path|
|
29
|
+
@_after_hooks[path] ||= []
|
30
|
+
@_after_hooks[path] << block
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def inherited(subclass)
|
35
|
+
super
|
36
|
+
subclass.instance_variable_set(:@_before_hooks, @_before_hooks&.dup || {})
|
37
|
+
subclass.instance_variable_set(:@_after_hooks, @_after_hooks&.dup || {})
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
module InstanceMethods
|
42
|
+
def call(env)
|
43
|
+
catch(:halt) do
|
44
|
+
req = Lennarb::Request.new(env)
|
45
|
+
res = Lennarb::Response.new
|
46
|
+
|
47
|
+
path = env[Rack::PATH_INFO]
|
48
|
+
|
49
|
+
execute_hooks(self.class.instance_variable_get(:@_before_hooks), '*', req, res)
|
50
|
+
|
51
|
+
execute_matching_hooks(self.class.instance_variable_get(:@_before_hooks), path, req, res)
|
52
|
+
|
53
|
+
status, headers, body = super
|
54
|
+
res.status = status
|
55
|
+
headers.each { |k, v| res[k] = v }
|
56
|
+
body.each { |chunk| res.write(chunk) }
|
57
|
+
|
58
|
+
execute_matching_hooks(self.class.instance_variable_get(:@_after_hooks), path, req, res)
|
59
|
+
|
60
|
+
execute_hooks(self.class.instance_variable_get(:@_after_hooks), '*', req, res)
|
61
|
+
|
62
|
+
res.finish
|
63
|
+
end
|
64
|
+
rescue StandardError => e
|
65
|
+
handle_error(e)
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
def execute_hooks(hooks, path, req, res)
|
71
|
+
return unless hooks&.key?(path)
|
72
|
+
|
73
|
+
hooks[path].each do |hook|
|
74
|
+
instance_exec(req, res, &hook)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def execute_matching_hooks(hooks, current_path, req, res)
|
79
|
+
return unless hooks
|
80
|
+
|
81
|
+
hooks.each do |pattern, pattern_hooks|
|
82
|
+
next if pattern == '*'
|
83
|
+
|
84
|
+
next unless matches_pattern?(pattern, current_path)
|
85
|
+
|
86
|
+
pattern_hooks.each do |hook|
|
87
|
+
instance_exec(req, res, &hook)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def matches_pattern?(pattern, path)
|
93
|
+
return true if pattern == path
|
94
|
+
|
95
|
+
pattern_parts = pattern.split('/')
|
96
|
+
path_parts = path.split('/')
|
97
|
+
|
98
|
+
return false if pattern_parts.length != path_parts.length
|
99
|
+
|
100
|
+
pattern_parts.zip(path_parts).all? do |pattern_part, path_part|
|
101
|
+
pattern_part.start_with?(':') || pattern_part == path_part
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def handle_error(error)
|
106
|
+
case error
|
107
|
+
when ArgumentError
|
108
|
+
[400, { 'Content-Type' => 'text/plain' }, ["Bad Request: #{error.message}"]]
|
109
|
+
else
|
110
|
+
[500, { 'Content-Type' => 'text/plain' }, ['Internal Server Error']]
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
Lennarb::Plugin.register(:hooks, Hooks)
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Lennarb
|
4
|
+
module Plugins
|
5
|
+
module Mount
|
6
|
+
def self.configure(app)
|
7
|
+
app.instance_variable_set(:@_mounted_apps, {})
|
8
|
+
app.extend(ClassMethods)
|
9
|
+
app.include(InstanceMethods)
|
10
|
+
end
|
11
|
+
|
12
|
+
module ClassMethods
|
13
|
+
def mount(app_class, at:)
|
14
|
+
raise ArgumentError, 'Expected a Lennarb class' unless app_class.is_a?(Class) && app_class <= Lennarb
|
15
|
+
raise ArgumentError, 'Mount path must start with /' unless at.start_with?('/')
|
16
|
+
|
17
|
+
@_mounted_apps ||= {}
|
18
|
+
normalized_path = normalize_mount_path(at)
|
19
|
+
|
20
|
+
mounted_app = app_class.new
|
21
|
+
mounted_app.freeze!
|
22
|
+
|
23
|
+
@_mounted_apps[normalized_path] = mounted_app
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def normalize_mount_path(path)
|
29
|
+
path.chomp('/')
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
module InstanceMethods
|
34
|
+
def call(env)
|
35
|
+
path_info = env[Rack::PATH_INFO]
|
36
|
+
|
37
|
+
self.class.instance_variable_get(:@_mounted_apps)&.each do |mount_path, app|
|
38
|
+
next unless path_info.start_with?("#{mount_path}/") || path_info == mount_path
|
39
|
+
|
40
|
+
env[Rack::PATH_INFO] = path_info[mount_path.length..]
|
41
|
+
env[Rack::PATH_INFO] = '/' if env[Rack::PATH_INFO].empty?
|
42
|
+
env[Rack::SCRIPT_NAME] = "#{env[Rack::SCRIPT_NAME]}#{mount_path}"
|
43
|
+
|
44
|
+
return app.call(env)
|
45
|
+
end
|
46
|
+
|
47
|
+
super
|
48
|
+
rescue StandardError => e
|
49
|
+
handle_error(e)
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def handle_error(error)
|
55
|
+
case error
|
56
|
+
when ArgumentError
|
57
|
+
[400, { 'content-type' => 'text/plain' }, ["Bad Request: #{error.message}"]]
|
58
|
+
else
|
59
|
+
[500, { 'content-type' => 'text/plain' }, ['Internal Server Error']]
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
Lennarb::Plugin.register(:mount, Mount)
|
65
|
+
end
|
66
|
+
end
|
data/lib/lennarb/request.rb
CHANGED
@@ -4,41 +4,80 @@
|
|
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
|
-
|
7
|
+
class Request < Rack::Request
|
8
|
+
# The environment variables of the request
|
9
|
+
#
|
10
|
+
# @returns [Hash]
|
11
|
+
#
|
12
|
+
attr_reader :env
|
13
|
+
|
14
|
+
# Initialize the request object
|
15
|
+
#
|
16
|
+
# @parameter [Hash] env
|
17
|
+
# @parameter [Hash] route_params
|
18
|
+
#
|
19
|
+
# @returns [Request]
|
20
|
+
#
|
21
|
+
def initialize(env, route_params = {})
|
22
|
+
super(env)
|
23
|
+
@route_params = route_params
|
24
|
+
end
|
25
|
+
|
26
|
+
# Get the request body
|
27
|
+
#
|
28
|
+
# @returns [String]
|
29
|
+
#
|
30
|
+
def params = @params ||= super.merge(@route_params)&.transform_keys(&:to_sym)
|
31
|
+
|
32
|
+
# Get the request path
|
33
|
+
#
|
34
|
+
# @returns [String]
|
35
|
+
#
|
36
|
+
def path = @path ||= super.split('?').first
|
37
|
+
|
38
|
+
# Read the body of the request
|
39
|
+
#
|
40
|
+
# @returns [String]
|
41
|
+
#
|
42
|
+
def body = @body ||= super.read
|
43
|
+
|
44
|
+
# Get the query parameters
|
45
|
+
#
|
46
|
+
# @returns [Hash]
|
47
|
+
#
|
48
|
+
def query_params
|
49
|
+
@query_params ||= Rack::Utils.parse_nested_query(query_string).transform_keys(&:to_sym)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Get the headers of the request
|
53
|
+
#
|
54
|
+
def headers
|
55
|
+
@headers ||= env.select { |key, _| key.start_with?('HTTP_') }
|
56
|
+
end
|
57
|
+
|
58
|
+
def ip = ip_address
|
59
|
+
def secure? = scheme == 'https'
|
60
|
+
def user_agent = headers['HTTP_USER_AGENT']
|
61
|
+
def accept = headers['HTTP_ACCEPT']
|
62
|
+
def referer = headers['HTTP_REFERER']
|
63
|
+
def host = headers['HTTP_HOST']
|
64
|
+
def content_length = headers['HTTP_CONTENT_LENGTH']
|
65
|
+
def content_type = headers['HTTP_CONTENT_TYPE']
|
66
|
+
def xhr? = headers['HTTP_X_REQUESTED_WITH']&.casecmp('XMLHttpRequest')&.zero?
|
67
|
+
|
68
|
+
def []=(key, value)
|
69
|
+
env[key] = value
|
70
|
+
end
|
71
|
+
|
72
|
+
def [](key)
|
73
|
+
env[key]
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def ip_address
|
79
|
+
forwarded_for = headers['HTTP_X_FORWARDED_FOR']
|
80
|
+
forwarded_for ? forwarded_for.split(',').first.strip : env['REMOTE_ADDR']
|
81
|
+
end
|
82
|
+
end
|
44
83
|
end
|