rs
1# frozen_string_literal: true
2
3module Net
4 module Hippie
5 # Rust-powered HTTP connection with debug logging support.
6 #
7 # RustConnection provides a high-performance HTTP client using Rust's reqwest library
8 # while maintaining full compatibility with the Ruby backend interface, including
9 # comprehensive debug logging that matches Net::HTTP's set_debug_output behavior.
10 #
11 # == Debug Logging
12 #
13 # When a logger is provided, RustConnection outputs detailed HTTP transaction logs
14 # in the same format as Net::HTTP:
15 #
16 # logger = File.open('http_debug.log', 'w')
17 # connection = RustConnection.new('https', 'api.example.com', 443, logger: logger)
18 #
19 # # Output format:
20 # # -> "GET https://api.example.com/users HTTP/1.1"
21 # # -> "accept: application/json"
22 # # -> "user-agent: net/hippie 2.0.0"
23 # # -> ""
24 # # <- "HTTP/1.1 200"
25 # # <- "content-type: application/json"
26 # # <- ""
27 # # <- "{\"users\":[...]}"
28 #
29 # @since 2.0.0
30 # @see RubyConnection The Ruby/net-http implementation
31 # @see Connection The backend abstraction layer
32 class RustConnection
33 def initialize(scheme, host, port, options = {})
34 @scheme = scheme
35 @host = host
36 @port = port
37 @options = options
38 @logger = options[:logger]
39
40 # Create the Rust client (simplified version for now)
41 @rust_client = Net::Hippie::RustClient.new
42 end
43
44 def run(request)
45 url = build_url_for(request.path)
46 headers = extract_headers(request)
47 body = request.body || ''
48 method = extract_method(request)
49
50 # Debug logging (mimics Net::HTTP's set_debug_output behavior)
51 log_request(method, url, headers, body) if @logger
52
53 begin
54 rust_response = @rust_client.public_send(method.downcase, url, headers, body)
55 response = RustBackend::ResponseAdapter.new(rust_response)
56
57 # Debug log response
58 log_response(response) if @logger
59
60 response
61 rescue => e
62 # Map Rust errors to Ruby equivalents
63 raise map_rust_error(e)
64 end
65 end
66
67 def build_url_for(path)
68 return path if path.start_with?('http')
69
70 port_suffix = (@port == 80 && @scheme == 'http') || (@port == 443 && @scheme == 'https') ? '' : ":#{@port}"
71 "#{@scheme}://#{@host}#{port_suffix}#{path}"
72 end
73
74 private
75
76 def extract_headers(request)
77 headers = {}
78 request.each_header do |key, value|
79 headers[key] = value
80 end
81 headers
82 end
83
84 def extract_method(request)
85 request.class.name.split('::').last.sub('HTTP', '').downcase
86 end
87
88 # Logs HTTP request details in Net::HTTP debug format.
89 #
90 # Outputs request line, headers, and body using the same format as
91 # Net::HTTP's set_debug_output for consistent debugging experience.
92 #
93 # @param method [String] HTTP method (GET, POST, etc.)
94 # @param url [String] Complete request URL
95 # @param headers [Hash] Request headers
96 # @param body [String] Request body content
97 # @since 2.0.0
98 def log_request(method, url, headers, body)
99 # Format similar to Net::HTTP's debug output
100 @logger << "-> \"#{method.upcase} #{url} HTTP/1.1\"\n"
101
102 # Log headers
103 headers.each do |key, value|
104 @logger << "-> \"#{key.downcase}: #{value}\"\n"
105 end
106
107 @logger << "-> \"\"\n" # Empty line
108
109 # Log body if present
110 if body && !body.empty?
111 @logger << "-> \"#{body}\"\n"
112 end
113
114 @logger.flush if @logger.respond_to?(:flush)
115 end
116
117 # Logs HTTP response details in Net::HTTP debug format.
118 #
119 # Outputs response status, headers, and body (truncated if large) using
120 # the same format as Net::HTTP's set_debug_output.
121 #
122 # @param response [RustBackend::ResponseAdapter] HTTP response object
123 # @since 2.0.0
124 def log_response(response)
125 # Format similar to Net::HTTP's debug output
126 @logger << "<- \"HTTP/1.1 #{response.code}\"\n"
127
128 # Log some common response headers if available
129 %w[content-type content-length location server date].each do |header|
130 value = response[header]
131 if value
132 @logger << "<- \"#{header}: #{value}\"\n"
133 end
134 end
135
136 @logger << "<- \"\"\n" # Empty line
137
138 # Log response body (truncated if too long)
139 body = response.body
140 if body && !body.empty?
141 display_body = body.length > 1000 ? "#{body[0...1000]}...[truncated]" : body
142 @logger << "<- \"#{display_body}\"\n"
143 end
144
145 @logger.flush if @logger.respond_to?(:flush)
146 end
147
148 def map_rust_error(error)
149 case error.message
150 when /Net::ReadTimeout/
151 Net::ReadTimeout.new
152 when /Net::OpenTimeout/
153 Net::OpenTimeout.new
154 when /Errno::ECONNREFUSED/
155 Errno::ECONNREFUSED.new
156 when /Errno::ECONNRESET/
157 Errno::ECONNRESET.new
158 when /timeout/i
159 Net::ReadTimeout.new
160 else
161 error
162 end
163 end
164 end
165 end
166end