lennarb 0.1.6 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,77 +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 used to manage the namespaces.
9
- #
10
- # @private `Since v0.1.0`
11
- #
12
- class NamespaceStack
13
- # @return [Array] The stack of namespaces
14
- #
15
- # @private
16
- #
17
- attr_reader :stack
18
-
19
- # The initialize method is used to initialize the stack of namespaces.
20
- #
21
- # @private
22
- #
23
- # @attibute stack [Array] The stack of namespaces
24
- #
25
- # @return [void]
26
- #
27
- def initialize = @stack = ['']
28
-
29
- # This method is used to push a prefix to the stack.
30
- #
31
- # @parameter prefix [String] The prefix to be pushed
32
- #
33
- # @return [void]
34
- #
35
- # ex.
36
- #
37
- # stack = NamespaceStack.new
38
- # stack.push('/users')
39
- # stack.current_prefix # => '/users'
40
- #
41
- # @see #resolve_prefix
42
- #
43
- def push(prefix)
44
- @stack.push(resolve_prefix(prefix))
45
- end
46
-
47
- # This method is used to remove the last prefix from the stack.
48
- #
49
- # @return [String] The popped prefix
50
- #
51
- def pop
52
- @stack.pop unless @stack.size == 1
53
- end
54
-
55
- # @return [String] The current prefix
56
- #
57
- def current_prefix = @stack.last
58
-
59
- # The to_s method is used to return the current prefix.
60
- #
61
- # @return [String] The current prefix
62
- #
63
- def to_s = current_prefix
64
-
65
- private
66
-
67
- # The resolve_prefix method is used to resolve the prefix.
68
- #
69
- # @parameter prefix [String] The prefix to be resolved
70
- # @return [String] The resolved prefix
71
- #
72
- def resolve_prefix(prefix)
73
- current_prefix + prefix
74
- end
75
- end
76
- end
77
- end
@@ -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