lennarb 0.1.7 → 0.4.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.
@@ -1,509 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Released under the MIT License.
4
- # Copyright, 2023, by Aristóteles Coutinho.
5
-
6
- module Lenna
7
- class Router
8
- # The Response class is responsible for managing the response.
9
- #
10
- # @attr headers [Hash] the response headers
11
- # @attr body [Array(String)] the response body
12
- # @attr status [Integer] the response status
13
- # @attr params [Hash] the response params
14
- #
15
- # @public `Since v0.1.0`
16
- #
17
- class Response
18
- # The status of the response.
19
- #
20
- # @return [Integer] the status of the Response
21
- #
22
- # @public
23
- #
24
- public attr_accessor :status
25
-
26
- # The body of the response.
27
- #
28
- # @return [Array(String)] the body of the Response
29
- #
30
- # @public
31
- #
32
- public attr_reader :body
33
-
34
- # The headers of the response.
35
- #
36
- # @return [Hash] the headers of the Response
37
- #
38
- # @public
39
- #
40
- public attr_reader :headers
41
-
42
- # The params of the response.
43
- #
44
- # @return [Hash] the params of the Response
45
- #
46
- # @public
47
- #
48
- public attr_reader :params
49
-
50
- # The length of the response.
51
- #
52
- # @return [Integer] the length of the Response
53
- #
54
- # @public
55
- #
56
- public attr_reader :length
57
-
58
- # This method will initialize the response.
59
- #
60
- # @parameter headers [Hash] the response headers, default is {}
61
- # @parameter status [Integer] the response status, default is 200
62
- # @parameter body [Array(String)] the response body, default is []
63
- # @parameter params [Hash] the response params, default is {}
64
- # @length [Integer] the response length, default is 0
65
- #
66
- # @return [void]
67
- #
68
- def initialize(headers = {}, status = 200, body = [])
69
- @params = {}
70
- @body = body
71
- @status = status
72
- @headers = headers
73
- @length = 0
74
- end
75
-
76
- # This method will set the params.
77
- #
78
- # @parameter params [Hash] the params to be used
79
- #
80
- # @return [void]
81
- #
82
- # @public
83
- #
84
- # ex.
85
- # response.params = { 'name' => 'John' }
86
- # # => { 'name' => 'John' }
87
- #
88
- def assign_params(params)
89
- params => ::Hash
90
-
91
- @params = params
92
- rescue ::NoMatchingPatternError
93
- raise ::ArgumentError, 'params must be a hash'
94
- end
95
- alias params= assign_params
96
-
97
- # Returns the response header corresponding to `key`.
98
- #
99
- # @parameter key [String] the header name
100
- #
101
- # @return [String] the header value
102
- #
103
- # @public
104
- #
105
- # ex.
106
- # res["Content-Type"] # => "text/html"
107
- # res["Content-Length"] # => "42"
108
- #
109
- def [](key) = @headers[key]
110
-
111
- # Thi method set the body value.
112
- #
113
- # @parameter value [Array(String)] the body value
114
- #
115
- # @return [void]
116
- #
117
- # @public
118
- #
119
- def assign_body(value) = put_body(value)
120
- alias body= assign_body
121
-
122
- # This method will set the header value.
123
- #
124
- # @parameter header [String] the header name
125
- # @parameter value [String] the header value
126
- #
127
- # @return [void]
128
- #
129
- # @public
130
- #
131
- # ex.
132
- # assign_header('X-Request-Id', '123')
133
- # # => '123'
134
- #
135
- # assign_header('X-Request-Id', '456')
136
- # # => ['123', '456']
137
- #
138
- # assign_header('X-Request-Id', ['456', '789'])
139
- # # => ['123', '456', '789']
140
- #
141
- def assign_header(key, value) = put_header(key, value)
142
- alias []= assign_header
143
-
144
- # Add multiple headers.
145
- #
146
- # @parameter headers [Hash] the headers
147
- # @return [void]
148
- #
149
- # ex.
150
- # headers = {
151
- # 'Content-Type' => 'application/json',
152
- # 'X-Request-Id' => '123'
153
- # }
154
- #
155
- def assign_headers(headers)
156
- headers => ::Hash
157
-
158
- headers.each { |key, value| put_header(key, value) }
159
- end
160
- alias headers= assign_headers
161
-
162
- # This method will get the content type.
163
- #
164
- # @return [String] the content type
165
- #
166
- # @public
167
- #
168
- def content_type = @headers['Content-Type']
169
-
170
- # This method will delete the header.
171
- #
172
- # @parameter header [String] the header name
173
- #
174
- # @return [void]
175
- #
176
- # @public
177
- #
178
- def remove_header(key) = delete_header(key)
179
-
180
- # This method will set the cookie.
181
- #
182
- # @parameter key [String] the key of the cookie
183
- # @return [void]
184
- # @parameter value [String] the value of the cookie
185
- #
186
- #
187
- # @public
188
- #
189
- # ex.
190
- # response.assign_cookie('foo', 'bar')
191
- # # => 'foo=bar'
192
- #
193
- # response.header['Set-Cookie']
194
- # # => 'foo=bar'
195
- #
196
- # response.assign_cookie('foo2', 'bar2')
197
- # # => 'foo=bar; foo2=bar2'
198
- # response.header['Set-Cookie']
199
- # # => 'foo=bar; foo2=bar2'
200
- #
201
- # response.assign_cookie('bar', {
202
- # domain: 'example.com',
203
- # path: '/',
204
- # # expires: Time.now + 24 * 60 * 60,
205
- # secure: true,
206
- # httponly: true
207
- # })
208
- #
209
- # response.header['Set-Cookie'].split('\n').last
210
- # # => 'bar=; domain=example.com; path=/; secure; HttpOnly'
211
- #
212
- # note:
213
- # This method doesn't sign and/or encrypt the cookie.
214
- # If you want to sign and/or encrypt the cookie, then you can use
215
- # the `Rack::Session::Cookie` middleware.
216
- #
217
- def assign_cookie(key, value)
218
- key => ::String
219
- value => ::String
220
-
221
- ::Rack::Utils.set_cookie_header!(@headers, key, value)
222
- rescue ::NoMatchingPatternError
223
- raise ::ArgumentError, 'key must be a string'
224
- end
225
-
226
- # This method will get all the cookies.
227
- #
228
- # @return [Hash] the cookies
229
- #
230
- # @public
231
- #
232
- def cookies = @headers['set-cookie']
233
-
234
- # This method will delete the cookie.
235
- #
236
- # @parameter key [String] the key of the cookie
237
- #
238
- # @return [void]
239
- #
240
- # @public
241
- #
242
- # ex.
243
- # response.delete_cookie('foo')
244
- # # => 'foo=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 -0000'
245
- #
246
- def delete_cookie(key)
247
- key => ::String
248
-
249
- ::Rack::Utils.delete_cookie_header!(@headers, key)
250
- rescue ::NoMatchingPatternError
251
- raise ::ArgumentError, 'key must be a string'
252
- end
253
-
254
- # This method will set redirect location. The status will be set to 302.
255
- #
256
- # @parameter location [String] the redirect location
257
- # @parameter status [Integer] the redirect status, default is 302.
258
- #
259
- # @return [void]
260
- #
261
- # @public
262
- #
263
- def redirect(location, status: 302)
264
- location => ::String
265
-
266
- put_header('Location', location)
267
- put_status(status)
268
-
269
- finish!
270
- rescue ::NoMatchingPatternError
271
- raise ::ArgumentError, 'location must be a string'
272
- end
273
-
274
- # This method will finish the response.
275
- #
276
- # @return [void]
277
- #
278
- # @public
279
- #
280
- def finish = finish!
281
-
282
- # This method will set the response content type.
283
- #
284
- # @parameter type [String] the response content type
285
- # @parameter charset [Hash] the response charset
286
- #
287
- # @return [void]
288
- #
289
- # @public
290
- #
291
- def assign_content_type(type, charset: nil)
292
- type => ::String
293
-
294
- case charset
295
- in ::String then put_header('Content-Type', "#{type}; charset=#{charset}")
296
- else put_header('Content-Type', type)
297
- end
298
- rescue ::NoMatchingPatternError
299
- raise ::ArgumentError, 'type must be a string'
300
- end
301
-
302
- # This method will set the response data and finish the response.
303
- #
304
- # @parameter data [Hash | Array] the response data
305
- #
306
- # @return [void]
307
- #
308
- # @public
309
- #
310
- # ex.
311
- # response.json({ foo: 'bar' })
312
- # # => { foo: 'bar' }
313
- #
314
- # response.json([{ foo: 'bar' }])
315
- # # => [{ foo: 'bar' }]
316
- #
317
- def json(data = {}, status: 200)
318
- data => ::Array | ::Hash
319
-
320
- put_status(status)
321
- put_header('Content-Type', 'application/json')
322
- put_body(data.to_json)
323
-
324
- finish!
325
- end
326
-
327
- # Set the response content type to text/html.
328
- #
329
- # @parameter str [String] the response body
330
- #
331
- # @return [void]
332
- #
333
- # @public
334
- #
335
- def html(str = nil, status: 200)
336
- put_status(status)
337
- put_header('Content-Type', 'text/html')
338
- put_body(str)
339
-
340
- finish!
341
- end
342
-
343
- # This method will render the template.
344
- #
345
- # @parameter template_nam [String] the template name
346
- # @parameter path [String] the template path, default is 'views'
347
- # @parameter locals [Hash] the template locals
348
- #
349
- # @return [void | Exception]
350
- #
351
- # @public
352
- #
353
- # ex.
354
- # render('index')
355
- # # => Render the template `views/index.html.erb`
356
- #
357
- # render('users/index')
358
- # # => Render the template `views/users/index.html.erb`
359
- #
360
- # render('index', path: 'app/views/users')
361
- # # => Render the template `app/views/users/index.html.erb`
362
- #
363
- # render('index', locals: { name: 'John' })
364
- # # => Render the template `views/index.html.erb` with the local
365
- # # variable `name` set to 'John'
366
- #
367
- def render(template_name, path: 'views', locals: {}, status: 200)
368
- template_path = ::File.join(path, "#{template_name}.html.erb")
369
-
370
- # Check if the template exists
371
- unless File.exist?(template_path)
372
- msg = "Template not found: #{template_path} 🤷‍♂️."
373
-
374
- # Oops! The template doesn't exist or the path is wrong.
375
- #
376
- # The template exists? 🤔
377
- # If you want to render a template from a custom path, then you
378
- # can pass the full path though the path: keyword argument instead
379
- # of just the name. For example:
380
- # render('index', path: 'app/views/users')
381
- raise msg
382
- end
383
-
384
- ::File
385
- .read(template_path)
386
- .then { |template| ::ERB.new(template).result_with_hash(locals) }
387
- .then { |erb_template| html(erb_template, status:) }
388
- end
389
-
390
- # Helper methods for the response.
391
- #
392
- # @return [void]
393
- #
394
- # @public
395
- #
396
- def not_found
397
- put_body(['Not Found'])
398
- put_status(404)
399
-
400
- finish!
401
- end
402
-
403
- private
404
-
405
- # This method will get the response status.
406
- #
407
- # @return [Integer] the response status
408
- #
409
- def put_status(value)
410
- value => ::Integer
411
-
412
- self.status = value
413
- rescue ::NoMatchingPatternError
414
- raise ::ArgumentError, 'status must be an integer'
415
- end
416
-
417
- # This method will set the body.
418
- #
419
- # @parameter body [Array(String)] the body to be used
420
- #
421
- # @return [void]
422
- #
423
- def put_body(value)
424
- value => ::String | ::Array
425
-
426
- case value
427
- in ::String then @body = [value]
428
- in ::Array then @body = value
429
- end
430
- rescue ::NoMatchingPatternError
431
- raise ::ArgumentError, 'body must be a string or an array'
432
- end
433
-
434
- # @parameter key [String] the header name
435
- # @parameter value [String] the value to be used
436
- #
437
- # @return [void]
438
- #
439
- def put_header(key, value)
440
- raise ::ArgumentError, 'key must be a string' unless key.is_a?(::String)
441
-
442
- unless value.is_a?(::String) || value.is_a?(::Array)
443
- raise ::ArgumentError, 'value must be a string or an array'
444
- end
445
-
446
- header_value = @headers[key]
447
-
448
- new_values = ::Array.wrap(value)
449
- existing_values = ::Array.wrap(header_value)
450
-
451
- @headers[key] = (existing_values + new_values).uniq.join(', ')
452
- end
453
-
454
- # This method will delete a header by key.
455
- #
456
- # @parameter key [String] the header name
457
- #
458
- # @return [void]
459
- #
460
- def delete_header(key) = @headers.delete(key)
461
-
462
- # @parameter value [String] the redirect location
463
- #
464
- # @return [void]
465
- #
466
- def location!(value)
467
- value => ::String
468
-
469
- put_header('Location', value)
470
- end
471
-
472
- # This method will finish the response.
473
- #
474
- # @return [void]
475
- #
476
- def finish!
477
- default_router_header!
478
- default_content_length! unless @headers['Content-Length']
479
- default_html_content_type! unless @headers['Content-Type']
480
-
481
- [@status, @headers, @body]
482
- end
483
-
484
- # This method will set the response default html content type.
485
- #
486
- # @return [void]
487
- #
488
- def default_html_content_type!
489
- put_header('Content-Type', 'text/html')
490
- end
491
-
492
- # This method will set the response default content length.
493
- #
494
- # @return [void]
495
- #
496
- def default_content_length!
497
- put_header('Content-Length', @body.join.size.to_s)
498
- end
499
-
500
- # This method will set the response default router header.
501
- #
502
- # @return [void]
503
- #
504
- def default_router_header!
505
- put_header('Server', "Lennarb VERSION #{::Lennarb::VERSION}")
506
- end
507
- end
508
- end
509
- end
@@ -1,68 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Released under the MIT License.
4
- # Copyright, 2023, by Aristóteles Coutinho.
5
-
6
- module Lenna
7
- class Router
8
- # This class is responsible for matching the request path
9
- # to an endpoint and executing the endpoint action.
10
- #
11
- # @private Since `v0.1.0`
12
- #
13
- # This will match the request path to an endpoint and execute
14
- # the endpoint action.
15
- #
16
- class RouteMatcher
17
- # @parameter root_node [Lenna::Node] The root node
18
- #
19
- def initialize(root_node) = @root_node = root_node
20
-
21
- # This method will match the request path to an endpoint and execute
22
- # the endpoint action.
23
- #
24
- # @parameter req [Lenna::Request] The request object
25
- # @parameter res [Lenna::Response] The response object
26
- # @return [Lenna::Response] The response object
27
- #
28
- def match_and_execute_route(req, res)
29
- params = {}
30
- path_parts = split_path(req.path_info)
31
- endpoint = find_endpoint(@root_node, path_parts, params)
32
-
33
- if endpoint && (action = endpoint[req.request_method])
34
- req.assign_params(params)
35
- action.call(req, res)
36
- else
37
- res.not_found
38
- end
39
- end
40
-
41
- private
42
-
43
- def split_path(path) = path.split('/').reject(&:empty?)
44
-
45
- # @parameter node [Lenna::Node] The node to search
46
- # @parameter parts [Array] The path parts
47
- # @parameter params [Hash] The params hash
48
- #
49
- # @return [Lenna::Node] The node that matches the path
50
- #
51
- # This method is recursive.
52
- #
53
- def find_endpoint(node, parts, params)
54
- return node.endpoint if parts.empty?
55
-
56
- part = parts.shift
57
- child_node = node.children[part]
58
-
59
- if child_node.nil? && (placeholder_node = node.children[:placeholder])
60
- params[placeholder_node.placeholder_name] = part
61
- child_node = placeholder_node
62
- end
63
-
64
- find_endpoint(child_node, parts, params) if child_node
65
- end
66
- end
67
- end
68
- end