lennarb 1.4.1 → 1.5.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.
@@ -0,0 +1,117 @@
1
+ module Lennarb
2
+ module Middleware
3
+ # Request logger for the Lennarb framework
4
+ # Logs HTTP request details with color formatting
5
+ #
6
+ # @example Output:
7
+ # GET /users/123 (30ms)
8
+ # Status: 200 OK
9
+ # Params: {"id"=>"123", "password"=>"[FILTERED]"}
10
+ #
11
+ class RequestLogger
12
+ def initialize(app)
13
+ @app = app
14
+ end
15
+
16
+ # Get logger from application configuration
17
+ def logger = Lennarb::App.app.config.logger
18
+
19
+ # Process the request and log information
20
+ #
21
+ # @param [Hash] env Rack environment
22
+ # @return [Array] Rack response [status, headers, body]
23
+ def call(env)
24
+ request = Lennarb::Request.new(env)
25
+ start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
26
+
27
+ status, headers, body = @app.call(env)
28
+
29
+ duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
30
+
31
+ log_request(request, status, headers, duration)
32
+
33
+ [status, headers, body]
34
+ end
35
+
36
+ private
37
+
38
+ # Format duration in a human-readable way
39
+ #
40
+ # @param [Float] seconds Duration in seconds
41
+ # @return [String] Formatted duration
42
+ def format_duration(seconds)
43
+ if seconds < 1
44
+ "#{(seconds * 1000).round}ms"
45
+ elsif seconds < 60
46
+ format("%.2fs", seconds)
47
+ else
48
+ minutes = (seconds / 60).to_i
49
+ seconds = (seconds % 60).round
50
+ "#{minutes}m #{seconds}s"
51
+ end
52
+ end
53
+
54
+ # Log the complete request
55
+ def log_request(request, status, headers, duration)
56
+ logger.info { request_line(request, duration, status) }
57
+
58
+ logger.info { status_line(status) }
59
+
60
+ if request.params.any?
61
+ logger.info { params_line(request.params) }
62
+ end
63
+
64
+ if headers["Location"]
65
+ logger.info { redirect_line(headers["Location"]) }
66
+ end
67
+ end
68
+
69
+ # Format the request line
70
+ def request_line(request, duration, status)
71
+ method = request.request_method
72
+ path = filter_path(request.path)
73
+ duration_text = "(#{format_duration(duration)})"
74
+
75
+ "#{method} #{path} #{duration_text}".colorize(status_to_color(status)).bold
76
+ end
77
+
78
+ # Format the status line
79
+ def status_line(status)
80
+ status_text = "#{status} #{Rack::Utils::HTTP_STATUS_CODES[status]}"
81
+ "Status: #{status_text}".colorize(status_to_color(status)).bold
82
+ end
83
+
84
+ # Format the parameters line
85
+ def params_line(params)
86
+ filtered = filter_params(params)
87
+ "Params: #{filtered.inspect}"
88
+ end
89
+
90
+ # Format the redirect line
91
+ def redirect_line(location)
92
+ "Redirect: #{location}".colorize(:yellow)
93
+ end
94
+
95
+ # Filter the request path
96
+ def filter_path(path)
97
+ path
98
+ end
99
+
100
+ # Filter request parameters
101
+ def filter_params(params)
102
+ ParameterFilter.new.filter(params)
103
+ end
104
+
105
+ # Determine color based on HTTP status
106
+ def status_to_color(status)
107
+ case status
108
+ when 200..299 then :green
109
+ when 300..399 then :yellow
110
+ when 400..499 then :magenta
111
+ when 500..599 then :red
112
+ else :white
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,56 @@
1
+ module Lennarb
2
+ # Basic middleware stack implementation.
3
+ #
4
+ class MiddlewareStack
5
+ attr_reader :app
6
+
7
+ # The app's middleware stack.
8
+ #
9
+ # @param [Rack::Builder] app
10
+ #
11
+ def initialize(app = nil)
12
+ @app = app
13
+ @store = []
14
+ end
15
+
16
+ # Insert a middleware in the stack.
17
+ #
18
+ # @param [Class] middleware
19
+ # @param [Array] args
20
+ # @param [Proc] block
21
+ #
22
+ # @retrn [void]
23
+ #
24
+ def use(middleware, *args, &block)
25
+ @store << [middleware, args, block]
26
+ end
27
+
28
+ # Add a middleware to the beginning of the stack.
29
+ #
30
+ # @param [Class] middleware
31
+ # @param [Array] args
32
+ # @param [Proc] block
33
+ #
34
+ # @retrn [void]
35
+ #
36
+ def unshift(middleware, *args, &block)
37
+ @store.unshift([middleware, args, block])
38
+ end
39
+
40
+ # Clear the middleware stack.
41
+ #
42
+ # @retrn [void]
43
+ #
44
+ def clear
45
+ @store.clear
46
+ end
47
+
48
+ # Convert the middleware stack to an array.
49
+ #
50
+ # @retrn [Array]
51
+ #
52
+ def to_a
53
+ @store
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,76 @@
1
+ module Lennarb
2
+ # Filtra parâmetros sensíveis de logs e exceções.
3
+ # Útil para evitar o vazamento de informações confidenciais.
4
+ #
5
+ # Por padrão, as seguintes chaves de parâmetros são filtradas:
6
+ #
7
+ # - `passw`
8
+ # - `email`
9
+ # - `secret`
10
+ # - `token`
11
+ # - `_key`
12
+ # - `crypt`
13
+ # - `salt`
14
+ # - `certificate`
15
+ # - `otp`
16
+ # - `ssn`
17
+ # - `cvv`
18
+ # - `cvc`
19
+ # - `signature`
20
+ #
21
+ # @example
22
+ # filter = Lennarb::ParameterFilter.new
23
+ # filter.filter({ password: "secret", user: { email: "test@example.com" } })
24
+ # # => { password: "[filtered]", user: { email: "[filtered]" } }
25
+ #
26
+ class ParameterFilter
27
+ # @api private
28
+ DEFAULT_MASK = "[FILTERED]"
29
+
30
+ # @api private
31
+ DEFAULT_FILTERS = %w[
32
+ passw email secret token _key crypt salt certificate otp ssn cvv cvc
33
+ signature
34
+ ].freeze
35
+
36
+ # Inicializa um novo filtro de parâmetros
37
+ #
38
+ # @param [Array<String, Regexp>] filters Lista de padrões para filtrar
39
+ def initialize(filters = DEFAULT_FILTERS)
40
+ @filter = Regexp.union(filters.map(&:to_s))
41
+ end
42
+
43
+ # Filtra os parâmetros conforme o filtro configurado
44
+ #
45
+ # @param [Hash, Array] params Parâmetros a serem filtrados
46
+ # @param [String] mask Valor que substituirá os parâmetros filtrados
47
+ # @return [Hash, Array] Parâmetros filtrados
48
+ def filter(params, mask: DEFAULT_MASK)
49
+ filter_object(params.dup, mask)
50
+ end
51
+
52
+ private
53
+
54
+ # Filtra recursivamente um objeto (hash ou array)
55
+ #
56
+ # @param [Object] object Objeto a ser filtrado
57
+ # @param [String] mask Valor que substituirá os parâmetros filtrados
58
+ # @return [Object] Objeto filtrado
59
+ def filter_object(object, mask)
60
+ case object
61
+ when Hash
62
+ object.each do |key, value|
63
+ object[key] = if key.to_s.match?(@filter)
64
+ mask
65
+ else
66
+ filter_object(value, mask)
67
+ end
68
+ end
69
+ when Array
70
+ object = object.map { filter_object(it, mask) }
71
+ end
72
+
73
+ object
74
+ end
75
+ end
76
+ end
@@ -1,16 +1,18 @@
1
1
  module Lennarb
2
+ # Request object
3
+ #
2
4
  class Request < Rack::Request
3
5
  # The environment variables of the request
4
6
  #
5
- # @returns [Hash]
7
+ # @return [Hash]
6
8
  attr_reader :env
7
9
 
8
10
  # Initialize the request object
9
11
  #
10
- # @parameter [Hash] env
11
- # @parameter [Hash] route_params
12
+ # @param [Hash] env
13
+ # @param [Hash] route_params
12
14
  #
13
- # @returns [Request]
15
+ # @return [Request]
14
16
  #
15
17
  def initialize(env, route_params = {})
16
18
  super(env)
@@ -19,7 +21,7 @@ module Lennarb
19
21
 
20
22
  # Get the request parameters merged with route parameters
21
23
  #
22
- # @returns [Hash]
24
+ # @return [Hash]
23
25
  #
24
26
  def params
25
27
  @params ||= super.merge(@route_params)&.transform_keys(&:to_sym)
@@ -27,7 +29,7 @@ module Lennarb
27
29
 
28
30
  # Get the request path without query string
29
31
  #
30
- # @returns [String]
32
+ # @return [String]
31
33
  #
32
34
  def path
33
35
  @path ||= super.split("?").first
@@ -35,7 +37,7 @@ module Lennarb
35
37
 
36
38
  # Read the body of the request
37
39
  #
38
- # @returns [String]
40
+ # @return [String]
39
41
  #
40
42
  def body
41
43
  @body ||= super.read
@@ -43,7 +45,7 @@ module Lennarb
43
45
 
44
46
  # Get the query parameters
45
47
  #
46
- # @returns [Hash]
48
+ # @return [Hash]
47
49
  #
48
50
  def query_params
49
51
  @query_params ||= Rack::Utils.parse_nested_query(query_string || "").transform_keys(&:to_sym)
@@ -51,9 +53,9 @@ module Lennarb
51
53
 
52
54
  # Set a value in the environment
53
55
  #
54
- # @parameter [String] key
55
- # @parameter [Object] value
56
- # @returns [Object] the value
56
+ # @param [String] key
57
+ # @param [Object] value
58
+ # @return [Object] the value
57
59
  #
58
60
  def []=(key, value)
59
61
  env[key] = value
@@ -61,8 +63,8 @@ module Lennarb
61
63
 
62
64
  # Get a value from the environment
63
65
  #
64
- # @parameter [String] key
65
- # @returns [Object]
66
+ # @param [String] key
67
+ # @return [Object]
66
68
  #
67
69
  def [](key)
68
70
  env[key]
@@ -70,15 +72,20 @@ module Lennarb
70
72
 
71
73
  # Get the headers of the request
72
74
  #
73
- # @returns [Hash]
75
+ # @return [Hash]
74
76
  #
75
77
  def headers
76
- @headers ||= env.select { |key, _| key.start_with?("HTTP_") }
78
+ @headers ||= env.each_with_object({}) do |(key, value), result|
79
+ if key.start_with?("HTTP_")
80
+ header_name = key.sub("HTTP_", "").split("_").map(&:capitalize).join("-")
81
+ result[header_name] = value
82
+ end
83
+ end
77
84
  end
78
85
 
79
86
  # Get the client IP address
80
87
  #
81
- # @returns [String]
88
+ # @return [String]
82
89
  #
83
90
  def ip
84
91
  ip_address
@@ -86,7 +93,7 @@ module Lennarb
86
93
 
87
94
  # Check if the request is secure (HTTPS)
88
95
  #
89
- # @returns [Boolean]
96
+ # @return [Boolean]
90
97
  #
91
98
  def secure?
92
99
  scheme == "https"
@@ -96,7 +103,7 @@ module Lennarb
96
103
 
97
104
  # Get the user agent
98
105
  #
99
- # @returns [String, nil]
106
+ # @return [String, nil]
100
107
  #
101
108
  def user_agent
102
109
  env["HTTP_USER_AGENT"]
@@ -104,7 +111,7 @@ module Lennarb
104
111
 
105
112
  # Get the accept header
106
113
  #
107
- # @returns [String, nil]
114
+ # @return [String, nil]
108
115
  #
109
116
  def accept
110
117
  env["HTTP_ACCEPT"]
@@ -112,7 +119,7 @@ module Lennarb
112
119
 
113
120
  # Get the referer header
114
121
  #
115
- # @returns [String, nil]
122
+ # @return [String, nil]
116
123
  #
117
124
  def referer
118
125
  env["HTTP_REFERER"]
@@ -120,7 +127,7 @@ module Lennarb
120
127
 
121
128
  # Get the host header
122
129
  #
123
- # @returns [String, nil]
130
+ # @return [String, nil]
124
131
  #
125
132
  def host
126
133
  env["HTTP_HOST"]
@@ -128,7 +135,7 @@ module Lennarb
128
135
 
129
136
  # Get the content length header
130
137
  #
131
- # @returns [String, nil]
138
+ # @return [String, nil]
132
139
  #
133
140
  def content_length
134
141
  env["HTTP_CONTENT_LENGTH"]
@@ -136,7 +143,7 @@ module Lennarb
136
143
 
137
144
  # Get the content type header
138
145
  #
139
- # @returns [String, nil]
146
+ # @return [String, nil]
140
147
  #
141
148
  def content_type
142
149
  env["HTTP_CONTENT_TYPE"]
@@ -144,7 +151,7 @@ module Lennarb
144
151
 
145
152
  # Check if the request is an XHR request
146
153
  #
147
- # @returns [Boolean]
154
+ # @return [Boolean]
148
155
  #
149
156
  def xhr?
150
157
  env["HTTP_X_REQUESTED_WITH"]&.casecmp("XMLHttpRequest")&.zero? || false
@@ -152,7 +159,7 @@ module Lennarb
152
159
 
153
160
  # Check if the request is a JSON request
154
161
  #
155
- # @returns [Boolean]
162
+ # @return [Boolean]
156
163
  #
157
164
  def json?
158
165
  content_type&.include?("application/json")
@@ -160,12 +167,11 @@ module Lennarb
160
167
 
161
168
  # Parse JSON body if content type is application/json
162
169
  #
163
- # @returns [Hash, nil]
170
+ # @return [Hash, nil]
164
171
  #
165
172
  def json_body
166
173
  return nil unless json?
167
174
  @json_body ||= begin
168
- require "json"
169
175
  JSON.parse(body, symbolize_names: true)
170
176
  rescue JSON::ParserError
171
177
  nil
@@ -174,7 +180,7 @@ module Lennarb
174
180
 
175
181
  # Check if the request is an AJAX request (alias for xhr?)
176
182
  #
177
- # @returns [Boolean]
183
+ # @return [Boolean]
178
184
  #
179
185
  def ajax?
180
186
  xhr?
@@ -182,19 +188,21 @@ module Lennarb
182
188
 
183
189
  # Get the requested format (.html, .json, etc)
184
190
  #
185
- # @returns [Symbol, nil]
191
+ # @return [Symbol, nil]
186
192
  #
187
193
  def format
188
- path_info = env["PATH_INFO"]
189
- return nil unless path_info.include?(".")
194
+ @format ||= begin
195
+ path_info = env["PATH_INFO"]
196
+ return nil unless path_info.include?(".")
190
197
 
191
- extension = File.extname(path_info).delete(".")
192
- extension.empty? ? nil : extension.to_sym
198
+ extension = File.extname(path_info).delete(".")
199
+ extension.empty? ? nil : extension.to_sym
200
+ end
193
201
  end
194
202
 
195
203
  # Check if the request is a GET request
196
204
  #
197
- # @returns [Boolean]
205
+ # @return [Boolean]
198
206
  #
199
207
  def get?
200
208
  request_method == "GET"
@@ -202,7 +210,7 @@ module Lennarb
202
210
 
203
211
  # Check if the request is a POST request
204
212
  #
205
- # @returns [Boolean]
213
+ # @return [Boolean]
206
214
  #
207
215
  def post?
208
216
  request_method == "POST"
@@ -210,7 +218,7 @@ module Lennarb
210
218
 
211
219
  # Check if the request is a PUT request
212
220
  #
213
- # @returns [Boolean]
221
+ # @return [Boolean]
214
222
  #
215
223
  def put?
216
224
  request_method == "PUT"
@@ -218,7 +226,7 @@ module Lennarb
218
226
 
219
227
  # Check if the request is a DELETE request
220
228
  #
221
- # @returns [Boolean]
229
+ # @return [Boolean]
222
230
  #
223
231
  def delete?
224
232
  request_method == "DELETE"
@@ -226,7 +234,7 @@ module Lennarb
226
234
 
227
235
  # Check if the request is a HEAD request
228
236
  #
229
- # @returns [Boolean]
237
+ # @return [Boolean]
230
238
  #
231
239
  def head?
232
240
  request_method == "HEAD"
@@ -234,7 +242,7 @@ module Lennarb
234
242
 
235
243
  # Check if the request is a PATCH request
236
244
  #
237
- # @returns [Boolean]
245
+ # @return [Boolean]
238
246
  #
239
247
  def patch?
240
248
  request_method == "PATCH"
@@ -244,11 +252,13 @@ module Lennarb
244
252
 
245
253
  # Get the client IP address
246
254
  #
247
- # @returns [String]
255
+ # @return [String]
248
256
  #
249
257
  def ip_address
250
258
  forwarded_for = env["HTTP_X_FORWARDED_FOR"]
251
- forwarded_for ? forwarded_for.split(",").first.strip : env["REMOTE_ADDR"]
259
+ return forwarded_for.split(",").map(&:strip).first if forwarded_for
260
+
261
+ env["REMOTE_ADDR"]
252
262
  end
253
263
  end
254
264
  end
@@ -1,31 +1,61 @@
1
1
  module Lennarb
2
+ # Handles requests and executes routes with helpers and hooks.
3
+ #
2
4
  class RequestHandler
3
- Lennarb::Error = Class.new(StandardError)
4
-
5
5
  attr_reader :app
6
6
 
7
+ # Initialize the request handler
8
+ #
9
+ # @param [Lennarb::App] app The application instance
7
10
  def initialize(app)
8
11
  @app = app
9
12
  end
10
13
 
14
+ # Handle a request according to Rack interface
15
+ #
16
+ # @param [Hash] env The Rack environment
17
+ # @return [Array] Rack response [status, headers, body]
11
18
  def call(env)
12
19
  http_method = env[Rack::REQUEST_METHOD].to_sym
13
20
  parts = env[Rack::PATH_INFO].split("/").reject(&:empty?)
14
21
  block, params = app.routes.match_route(parts, http_method)
15
22
 
16
- unless block
17
- return [404, {"content-type" => CONTENT_TYPE[:TEXT]}, ["Not Found"]]
18
- end
23
+ return [404, {"content-type" => CONTENT_TYPE[:TEXT]}, ["Not Found"]] unless block
19
24
 
20
- req = Request.new(env, params)
25
+ req = Request.new(env, params || {})
21
26
  res = Response.new
22
27
 
23
28
  catch(:halt) do
24
- block.call(req, res)
29
+ context = create_context
30
+
31
+ Hooks.execute(context, app.class, :before, req, res)
32
+
33
+ context.instance_exec(req, res, params, &block)
34
+
35
+ Hooks.execute(context, app.class, :after, req, res)
36
+
25
37
  res.finish
26
- rescue Lennarb::Error => error
27
- [500, {"content-type" => CONTENT_TYPE[:TEXT]}, ["Internal Server Error (#{error.message})"]]
38
+ rescue Lennarb::Error => e
39
+ app.class.config.logger.error("Error: #{e.message}")
40
+ app.class.config.logger.error(e.backtrace.first)
41
+ [500, {"content-type" => "text/plain"}, ["Internal Server Error"]]
28
42
  end
29
43
  end
44
+
45
+ private
46
+
47
+ # Create a context object with app's helper methods
48
+ #
49
+ # @return [Object] A context object with helper methods
50
+ def create_context
51
+ context = Object.new
52
+
53
+ context.define_singleton_method(:app) { app }
54
+
55
+ helpers_module = Helpers.for(app.class)
56
+ context.extend(helpers_module) if helpers_module
57
+
58
+ context
59
+ end
30
60
  end
31
61
  end