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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8d16bb4723ee094f4db857aec2049a2bede8d109d7b35f12297f75ca117ac60c
4
- data.tar.gz: 8b908cc5d30da126c249eecbfeb419c68f64e306c018d68a3c897ac26f84722d
3
+ metadata.gz: 0fafe6e0a4a4fcb12d0cb7d75187b89c5ff2a8f4847bdab4db666d670fd1d40e
4
+ data.tar.gz: 43739df71271e6410f691b19d0e31c64ea5e883ae79150a3d0055d17a200fe8a
5
5
  SHA512:
6
- metadata.gz: b0455253e9ec8e0e684a7dce3d37b7d50e7480971c10f62ec1cecf822471732230e5fbccfaf7bf894585a63e47fcc7bae0020f465f22e3af90d4942d6f95c18c
7
- data.tar.gz: 2566e0d474c8b82c7a5607d16b69de2489133760732c9fa6ac9b8f7ab8bc7a36b38d40041f80ecfb4a61702280524d129c82cd4d5a67a1a7066cee37a521c218
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