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.
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,127 +15,144 @@ require_relative 'lennarb/route_node'
18
15
  require_relative 'lennarb/version'
19
16
 
20
17
  class Lennarb
21
- # Error class
22
- #
23
- class LennarbError < StandardError; end
24
-
25
- # @attribute [r] root
26
- # @returns [RouteNode]
27
- #
28
- attr_reader :_root
29
-
30
- # @attribute [r] applied_plugins
31
- # @returns [Array]
32
- #
33
- attr_reader :_applied_plugins
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
46
-
47
- # Split a path into parts
48
- #
49
- # @parameter [String] path
50
- #
51
- # @returns [Array] parts. Ex. ['users', ':id']
52
- #
53
- SplitPath = ->(path) { path.split('/').reject(&:empty?) }
54
- private_constant :SplitPath
55
-
56
- # Call the application
57
- #
58
- # @parameter [Hash] env
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]]
65
-
66
- block, params = @_root.match_route(parts, http_method)
67
- return [404, { 'content-type' => 'text/plain' }, ['Not Found']] unless block
68
-
69
- @res = Response.new
70
- req = Request.new(env, params)
71
-
72
- catch(:halt) do
73
- instance_exec(req, @res, &block)
74
- @res.finish
75
- end
76
- end
77
-
78
- # Freeze the routes
79
- #
80
- # @returns [void]
81
- #
82
- def freeze! = @_root.freeze
83
-
84
- # Add a routes
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)
98
-
99
- # Add plugin to extend the router
100
- #
101
- # @parameter [String] plugin_name
102
- # @parameter [args] *args
103
- # @parameter [Block] block
104
- #
105
- # @returns [void]
106
- #
107
- def plugin(plugin_name, *, &)
108
- return if @_applied_plugins.include?(plugin_name)
109
-
110
- plugin_module = Plugin.load(plugin_name)
111
- extend plugin_module::InstanceMethods if defined?(plugin_module::InstanceMethods)
112
- self.class.extend plugin_module::ClassMethods if defined?(plugin_module::ClassMethods)
113
- plugin_module.setup(self.class, *, &) if plugin_module.respond_to?(:setup)
114
-
115
- @_applied_plugins << plugin_name
116
- end
117
-
118
- # Merge the other RouteNode into the current one
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)
126
-
127
- @_root.merge!(other._root)
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?
128
81
  end
129
82
 
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
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
144
158
  end
data/license.md CHANGED
@@ -1,7 +1,6 @@
1
1
  # MIT License
2
2
 
3
- Copyright, 2023-2024, by Aristóteles Coutinho.
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
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lennarb
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.0
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aristóteles Coutinho
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-10-09 00:00:00.000000000 Z
11
+ date: 2024-11-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: colorize
@@ -180,8 +180,9 @@ files:
180
180
  - changelog.md
181
181
  - exe/lenna
182
182
  - lib/lennarb.rb
183
- - lib/lennarb/application/base.rb
184
183
  - lib/lennarb/plugin.rb
184
+ - lib/lennarb/plugins/hooks.rb
185
+ - lib/lennarb/plugins/mount.rb
185
186
  - lib/lennarb/request.rb
186
187
  - lib/lennarb/response.rb
187
188
  - lib/lennarb/route_node.rb
@@ -212,7 +213,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
212
213
  - !ruby/object:Gem::Version
213
214
  version: '0'
214
215
  requirements: []
215
- rubygems_version: 3.5.11
216
+ rubygems_version: 3.5.23
216
217
  signing_key:
217
218
  specification_version: 4
218
219
  summary: Lennarb provides a lightweight yet robust solution for web routing in Ruby,
@@ -1,283 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Released under the MIT License.
4
- # Copyright, 2023-2024, by Aristóteles Coutinho.
5
-
6
- require 'colorize'
7
-
8
- class Lennarb
9
- module Application
10
- class Base
11
- # @attribute [r] _route
12
- # @returns [RouteNode]
13
- #
14
- # @attribute [r] _middlewares
15
- # @returns [Array]
16
- #
17
- # @attribute [r] _global_after_hooks
18
- # @returns [Array]
19
- #
20
- # @attribute [r] _global_before_hooks
21
- # @returns [Array]
22
- #
23
- # @attribute [r] _after_hooks
24
- # @returns [RouteNode]
25
- #
26
- # @attribute [r] _before_hooks
27
- # @returns [RouteNode]
28
- #
29
- attr_accessor :_route, :_global_after_hooks, :_global_before_hooks, :_after_hooks, :_before_hooks
30
-
31
- # Initialize the Application
32
- #
33
- # @returns [Base]
34
- #
35
- class << self
36
- def inherited(subclass)
37
- super
38
- _applications << subclass
39
- subclass.instance_variable_set(:@_route, Lennarb.new)
40
- subclass.instance_variable_set(:@_middlewares, [])
41
- subclass.instance_variable_set(:@_global_after_hooks, [])
42
- subclass.instance_variable_set(:@_global_before_hooks, [])
43
- subclass.instance_variable_set(:@_after_hooks, Lennarb::RouteNode.new)
44
- subclass.instance_variable_set(:@_before_hooks, Lennarb::RouteNode.new)
45
- end
46
-
47
- def get(...) = @_route.get(...)
48
- def put(...) = @_route.put(...)
49
- def post(...) = @_route.post(...)
50
- def head(...) = @_route.head(...)
51
- def match(...) = @_route.match(...)
52
- def patch(...) = @_route.patch(...)
53
- def delete(...) = @_route.delete(...)
54
- def options(...) = @_route.options(...)
55
-
56
- # @returns [Array] middlewares
57
- #
58
- def _middlewares = @_middlewares ||= []
59
-
60
- # @returns [Array] applications
61
- #
62
- def _applications = @_applications ||= []
63
-
64
- # Mount a controller
65
- #
66
- # @parameter [Class] controller
67
- #
68
- def mount(controller_class)
69
- _applications << controller_class
70
- puts "Mounted controller: #{controller_class}"
71
- end
72
-
73
- # Use a middleware
74
- #
75
- # @parameter [Object] middleware
76
- # @parameter [Array] args
77
- # @parameter [Block] block
78
- #
79
- # @returns [Array] middlewares
80
- #
81
- def use(middleware, *args, &block)
82
- @_middlewares << [middleware, args, block]
83
- end
84
-
85
- # Add a before hook
86
- #
87
- # @parameter [String] path
88
- # @parameter [Block] block
89
- #
90
- def before(path = nil, &block)
91
- if path
92
- parts = path.split('/').reject(&:empty?)
93
- @_before_hooks.add_route(parts, :before, block)
94
- else
95
- @_global_before_hooks << block
96
- end
97
- end
98
-
99
- # Add a after hook
100
- #
101
- # @parameter [String] path
102
- # @parameter [Block] block
103
- #
104
- def after(path = nil, &block)
105
- if path
106
- parts = path.split('/').reject(&:empty?)
107
- @_after_hooks.add_route(parts, :after, block)
108
- else
109
- @_global_after_hooks << block
110
- end
111
- end
112
-
113
- # Run the Application
114
- #
115
- # @returns [Base] self
116
- #
117
- # When you use this method, the application will be frozen. And you can't add more routes after that.
118
- # This method is used to run the application in a Rack server so, you can use the `rackup` command
119
- # to run the application.
120
- # Ex. rackup -p 3000
121
- # This command will use the following middleware:
122
- # - Rack::ShowExceptions
123
- # - Rack::MethodOverride
124
- # - Rack::Head
125
- # - Rack::ContentLength
126
- #
127
- def run!
128
- stack = Rack::Builder.new
129
-
130
- use Rack::ShowExceptions if test? || development?
131
- use Rack::MethodOverride
132
- use Rack::Head
133
- use Rack::ContentLength
134
-
135
- _middlewares.each do |(middleware, args, block)|
136
- stack.use(middleware, *args, &block)
137
- end
138
-
139
- _applications.each do |app|
140
- app_route = app.instance_variable_get(:@_route)
141
-
142
- app_after_hooks = app.instance_variable_get(:@_after_hooks)
143
- app_before_hooks = app.instance_variable_get(:@_before_hooks)
144
- global_after_hooks = app.instance_variable_get(:@_global_after_hooks)
145
- global_before_hooks = app.instance_variable_get(:@_global_before_hooks)
146
-
147
- @_route.merge!(app_route)
148
- @_before_hooks.merge!(app_before_hooks)
149
- @_after_hooks.merge!(app_after_hooks)
150
-
151
- @_global_before_hooks.concat(global_before_hooks)
152
- @_global_after_hooks.concat(global_after_hooks)
153
- end
154
-
155
- stack.run ->(env) do
156
- catch(:halt) do
157
- execute_hooks(@_before_hooks, env, :before)
158
- res = @_route.call(env)
159
- execute_hooks(@_after_hooks, env, :after)
160
- res
161
- rescue StandardError => e
162
- render_error if production?
163
-
164
- puts e.message.red
165
- puts e.backtrace
166
- raise e
167
- end
168
- end
169
-
170
- @_route.freeze!
171
- stack.to_app
172
- end
173
-
174
- def test? = ENV['RACK_ENV'] == 'test' || ENV['LENNARB_ENV'] == 'test'
175
- def production? = ENV['RACK_ENV'] == 'production' || ENV['LENNARB_ENV'] == 'production'
176
- def development? = ENV['RACK_ENV'] == 'development' || ENV['LENNARB_ENV'] == 'development'
177
-
178
- # Render a not found
179
- #
180
- # @returns [void]
181
- #
182
- def render_not_found(content = nil)
183
- default = File.exist?('public/404.html')
184
- body = content || default || 'Not Found'
185
- throw :halt, [404, { 'content-type' => 'text/html' }, [body]]
186
- end
187
-
188
- # Render an error
189
- #
190
- # @returns [void]
191
- #
192
- def render_error(content = nil)
193
- default = File.exist?('public/500.html')
194
- body = content || default || 'Internal Server Error'
195
- throw :halt, [500, { 'content-type' => 'text/html' }, [body]]
196
- end
197
-
198
- # Redirect to a path
199
- #
200
- # @parameter [String] path
201
- # @parameter [Integer] status default is 302
202
- #
203
- def redirect(path, status = 302) = throw :halt, [status, { 'location' => path }, []]
204
-
205
- # Include a plugin in the application
206
- #
207
- # @parameter [String] plugin_name
208
- # @parameter [Array] args
209
- # @parameter [Block] block
210
- #
211
- # @returns [void]
212
- #
213
- def plugin(plugin_name, *, &)
214
- @_route.plugin(plugin_name, *, &)
215
- plugin_module = @_route.class::Plugin.load(plugin_name)
216
-
217
- include plugin_module::InstanceMethods if plugin_module.const_defined?(:InstanceMethods)
218
- extend plugin_module::ClassMethods if plugin_module.const_defined?(:ClassMethods)
219
- end
220
-
221
- private
222
-
223
- # Execute the hooks
224
- #
225
- # @parameter [RouteNode] hook_route
226
- # @parameter [Hash] env
227
- # @parameter [Symbol] action
228
- #
229
- # @returns [void]
230
- #
231
- def execute_hooks(hook_route, env, action)
232
- execute_global_hooks(env, action)
233
-
234
- execute_route_hooks(hook_route, env, action)
235
- end
236
-
237
- # Execute the global hooks
238
- #
239
- # @parameter [Hash] env
240
- # @parameter [Symbol] action
241
- #
242
- # @returns [void]
243
- #
244
- def execute_global_hooks(env, action)
245
- global_hooks = action == :before ? @_global_before_hooks : @_global_after_hooks
246
- global_hooks.each { |hook| hook.call(env) }
247
- end
248
-
249
- # Execute the route hooks
250
- #
251
- # @parameter [RouteNode] hook_route
252
- # @parameter [Hash] env
253
- # @parameter [Symbol] action
254
- #
255
- # @returns [void]
256
- #
257
- def execute_route_hooks(hook_route, env, action)
258
- parts = parse_path(env)
259
- return unless parts
260
-
261
- block, = hook_route.match_route(parts, action)
262
- block&.call(env)
263
- end
264
-
265
- # Parse the path
266
- #
267
- # @parameter [Hash] env
268
- #
269
- # @returns [Array] parts
270
- #
271
- def parse_path(env) = env[Rack::PATH_INFO]&.split('/')&.reject(&:empty?)
272
-
273
- # Check if the request is a HTML request
274
- #
275
- # @parameter [Hash] env
276
- #
277
- # @returns [Boolean]
278
- #
279
- def html_request?(env) = env['HTTP_ACCEPT']&.include?('text/html')
280
- end
281
- end
282
- end
283
- end