lennarb 1.2.0 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fd399f9aecbae5c09d1928317826fd82c62207a36d76ad6a1591a7757811d6e4
4
- data.tar.gz: 3f1ecf28399eeb28a8d739e50ffc01ecd0ae7328057c4132f15e6a12b65cd1e9
3
+ metadata.gz: 0fafe6e0a4a4fcb12d0cb7d75187b89c5ff2a8f4847bdab4db666d670fd1d40e
4
+ data.tar.gz: 43739df71271e6410f691b19d0e31c64ea5e883ae79150a3d0055d17a200fe8a
5
5
  SHA512:
6
- metadata.gz: b32f1d120bfd85de2512721631c90e95aa32622dfd69e41e90ce66f7d61b22db235b30c5de8329bfd1669dc4ca9b880bcb858068246362422e0c0b545b1e29af
7
- data.tar.gz: 28e13690c911ddb81d97c9e4fab95c270eb7e0367d2a26ea89b959252b3a449d611cb9dc067d0428e8959aa2beb96a9e9f3240462c97fbe480aecfd47ae169fd
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
@@ -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
- module Plugin
8
- @plugins = {}
9
-
10
- # Register a plugin
11
- #
12
- # @parameter [String] name
13
- # @parameter [Module] mod
14
- #
15
- # @returns [void]
16
- #
17
- def self.register(name, mod)
18
- @plugins[name] = mod
19
- end
20
-
21
- # Load a plugin
22
- #
23
- # @parameter [String] name
24
- #
25
- # @returns [Module] plugin
26
- #
27
- def self.load(name)
28
- @plugins[name] || raise(LennarbError, "Plugin #{name} did not register itself correctly")
29
- end
30
-
31
- # @returns [Hash] plugins
32
- #
33
- def self.plugins = @plugins
34
- end
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
@@ -4,41 +4,80 @@
4
4
  # Copyright, 2023-2024, by Aristóteles Coutinho.
5
5
 
6
6
  class Lennarb
7
- class Request < Rack::Request
8
- # Initialize the request object
9
- #
10
- # @parameter [Hash] env
11
- # @parameter [Hash] route_params
12
- #
13
- # @returns [Request]
14
- #
15
- def initialize(env, route_params = {})
16
- super(env)
17
- @route_params = route_params
18
- end
19
-
20
- # Get the request body
21
- #
22
- # @returns [String]
23
- #
24
- def params
25
- @params ||= super.merge(@route_params)
26
- end
27
-
28
- # Read the body of the request
29
- #
30
- # @returns [String]
31
- #
32
- def body = @body ||= super.read
33
-
34
- private
35
-
36
- # Get the query string parameters
37
- #
38
- # @returns [String]
39
- #
40
- def query_params
41
- @query_params ||= Rack::Utils.parse_nested_query(query_string)
42
- end
43
- end
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