summaryrefslogtreecommitdiff
path: root/lib/net/hippie/rust_connection.rb
blob: fc5d4c6e6bcdd0db01219abfebcdec14f8e244f4 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
# frozen_string_literal: true

require_relative 'rust_backend'

module Net
  module Hippie
    # Rust-powered HTTP connection with debug logging support.
    #
    # RustConnection provides a high-performance HTTP client using Rust's reqwest library
    # while maintaining full compatibility with the Ruby backend interface, including
    # comprehensive debug logging that matches Net::HTTP's set_debug_output behavior.
    #
    # == Debug Logging
    #
    # When a logger is provided, RustConnection outputs detailed HTTP transaction logs
    # in the same format as Net::HTTP:
    #
    #   logger = File.open('http_debug.log', 'w')
    #   connection = RustConnection.new('https', 'api.example.com', 443, logger: logger)
    #   
    #   # Output format:
    #   # -> "GET https://api.example.com/users HTTP/1.1"
    #   # -> "accept: application/json"
    #   # -> "user-agent: net/hippie 2.0.0"
    #   # -> ""
    #   # <- "HTTP/1.1 200"
    #   # <- "content-type: application/json"
    #   # <- ""
    #   # <- "{\"users\":[...]}"
    #
    # @since 2.0.0
    # @see RubyConnection The Ruby/net-http implementation
    # @see Connection The backend abstraction layer
    class RustConnection
      def initialize(scheme, host, port, options = {})
        @scheme = scheme
        @host = host
        @port = port
        @options = options
        @logger = options[:logger]
        
        # Create the Rust client (simplified version for now)
        @rust_client = Net::Hippie::RustClient.new
      end

      def run(request)
        url = build_url_for(request.path)
        headers = extract_headers(request)
        body = request.body || ''
        method = extract_method(request)

        # Debug logging (mimics Net::HTTP's set_debug_output behavior)
        log_request(method, url, headers, body) if @logger

        begin
          rust_response = @rust_client.public_send(method.downcase, url, headers, body)
          response = RustBackend::ResponseAdapter.new(rust_response)
          
          # Debug log response
          log_response(response) if @logger
          
          response
        rescue => e
          # Map Rust errors to Ruby equivalents
          raise map_rust_error(e)
        end
      end

      def build_url_for(path)
        return path if path.start_with?('http')

        port_suffix = (@port == 80 && @scheme == 'http') || (@port == 443 && @scheme == 'https') ? '' : ":#{@port}"
        "#{@scheme}://#{@host}#{port_suffix}#{path}"
      end

      private

      def extract_headers(request)
        headers = {}
        request.each_header do |key, value|
          headers[key] = value
        end
        headers
      end

      def extract_method(request)
        request.class.name.split('::').last.sub('HTTP', '').downcase
      end

      # Logs HTTP request details in Net::HTTP debug format.
      #
      # Outputs request line, headers, and body using the same format as
      # Net::HTTP's set_debug_output for consistent debugging experience.
      #
      # @param method [String] HTTP method (GET, POST, etc.)
      # @param url [String] Complete request URL
      # @param headers [Hash] Request headers
      # @param body [String] Request body content
      # @since 2.0.0
      def log_request(method, url, headers, body)
        # Format similar to Net::HTTP's debug output
        @logger << "-> \"#{method.upcase} #{url} HTTP/1.1\"\n"
        
        # Log headers
        headers.each do |key, value|
          @logger << "-> \"#{key.downcase}: #{value}\"\n"
        end
        
        @logger << "-> \"\"\n"  # Empty line
        
        # Log body if present
        if body && !body.empty?
          @logger << "-> \"#{body}\"\n"
        end
        
        @logger.flush if @logger.respond_to?(:flush)
      end

      # Logs HTTP response details in Net::HTTP debug format.
      #
      # Outputs response status, headers, and body (truncated if large) using
      # the same format as Net::HTTP's set_debug_output.
      #
      # @param response [RustBackend::ResponseAdapter] HTTP response object
      # @since 2.0.0
      def log_response(response)
        # Format similar to Net::HTTP's debug output
        @logger << "<- \"HTTP/1.1 #{response.code}\"\n"
        
        # Log some common response headers if available
        %w[content-type content-length location server date].each do |header|
          value = response[header]
          if value
            @logger << "<- \"#{header}: #{value}\"\n"
          end
        end
        
        @logger << "<- \"\"\n"  # Empty line
        
        # Log response body (truncated if too long)
        body = response.body
        if body && !body.empty?
          display_body = body.length > 1000 ? "#{body[0...1000]}...[truncated]" : body
          @logger << "<- \"#{display_body}\"\n"
        end
        
        @logger.flush if @logger.respond_to?(:flush)
      end

      def map_rust_error(error)
        case error.message
        when /Net::ReadTimeout/
          Net::ReadTimeout.new
        when /Net::OpenTimeout/
          Net::OpenTimeout.new
        when /Errno::ECONNREFUSED/
          Errno::ECONNREFUSED.new
        when /Errno::ECONNRESET/
          Errno::ECONNRESET.new
        when /timeout/i
          Net::ReadTimeout.new
        else
          error
        end
      end
    end
  end
end