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