lennarb 0.1.7 → 0.2.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,141 +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 Request class is responsible for managing the request.
9
- #
10
- # @attr headers [Hash] the request headers
11
- # @attr body [Hash] the request body
12
- # @attr params [Hash] the request params
13
- #
14
- # @public `Since v0.1.0`
15
- #
16
- class Request < ::Rack::Request
17
- # This method is used to set the request headers.
18
- #
19
- # @parameter name [String] the header name
20
- # @parameter value [String] the header value
21
- #
22
- # @return [String] the header value
23
- #
24
- # @public
25
- #
26
- # ex.
27
- # request.put_header('Foo', 'bar')
28
- #
29
- # request.headers
30
- # # => { 'Foo' => 'bar' }
31
- #
32
- def put_header(name, value) = headers[name] = value
33
-
34
- # This method is used to set the request params.
35
- #
36
- # @parameter params [Hash] the request params
37
- #
38
- # @return [Hash] the request params
39
- #
40
- # @public
41
- #
42
- def assign_params(params) = @params = params
43
-
44
- # This method is used to parse the body params.
45
- #
46
- # @return [Hash] the request params
47
- #
48
- # @public
49
- #
50
- def params = super.merge(parse_body_params)
51
-
52
- # This method rewinds the body
53
- #
54
- # @return [String] the request body content
55
- #
56
- # @public
57
- #
58
- def body
59
- super.rewind
60
- super.read
61
- end
62
-
63
- # This method returns the headers in a normalized way.
64
- #
65
- # @public
66
- #
67
- # @return [Hash] the request headers
68
- #
69
- # ex.
70
- # Turn this:
71
- # HTTP_FOO=bar Foo=bar
72
- #
73
- def headers
74
- content_type = env['CONTENT_TYPE']
75
- @headers ||= env.select { |k, _| k.start_with?('HTTP_') }
76
- .transform_keys { |k| format_header_name(k) }
77
-
78
- @headers['Content-Type'] = content_type if content_type
79
- @headers
80
- end
81
-
82
- # This method returns the request body like a json.
83
- #
84
- # @return [Hash] the request body
85
- #
86
- def json_body = @json_body ||= parse_body_params
87
-
88
- private
89
-
90
- # This method returns the media type.
91
- #
92
- # @return [String] the request media type
93
- #
94
- def media_type = headers['Content-Type']
95
-
96
- # This method parses the json body.
97
- #
98
- # @return [Hash] the request json body
99
- #
100
- def parse_json_body
101
- @parsed_json_body ||= ::JSON.parse(body)
102
- rescue ::JSON::ParserError
103
- {}
104
- end
105
-
106
- # This method parses the body params.
107
- #
108
- # @return [Hash] the request body params
109
- #
110
- def parse_body_params
111
- case media_type
112
- in 'application/json' then parse_json_body
113
- else post_params
114
- end
115
- end
116
-
117
- # This method parses the post params.
118
- #
119
- # @return [Hash] the request post params
120
- #
121
- def post_params
122
- @post_params ||=
123
- if body.empty?
124
- {}
125
- else
126
- ::Rack::Utils.parse_nested_query(body)
127
- end
128
- end
129
-
130
- # This method formats the header name.
131
- #
132
- # @parameter name [String] the header name
133
- #
134
- # @return [String] the formatted header name
135
- #
136
- def format_header_name(name)
137
- name.sub(/^HTTP_/, '').split('_').map(&:capitalize).join('-')
138
- end
139
- end
140
- end
141
- end
@@ -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