lennarb 1.2.0 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
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