diff options
Diffstat (limited to 'lib/net')
| -rw-r--r-- | lib/net/hippie.rb | 125 | ||||
| -rw-r--r-- | lib/net/hippie/client.rb | 190 | ||||
| -rw-r--r-- | lib/net/hippie/connection.rb | 118 | ||||
| -rw-r--r-- | lib/net/hippie/content_type_mapper.rb | 68 | ||||
| -rw-r--r-- | lib/net/hippie/rust_backend.rb | 141 |
5 files changed, 626 insertions, 16 deletions
diff --git a/lib/net/hippie.rb b/lib/net/hippie.rb index d057f52..0a504cf 100644 --- a/lib/net/hippie.rb +++ b/lib/net/hippie.rb @@ -13,8 +13,45 @@ require 'net/hippie/content_type_mapper' require 'net/hippie/rust_backend' module Net - # net/http for hippies. + # Net::Hippie is a lightweight wrapper around Ruby's net/http library that simplifies + # HTTP requests with JSON-first defaults and optional high-performance Rust backend. + # + # @since 0.1.0 + # + # == Features + # + # * JSON-first API with automatic content-type handling + # * Built-in retry logic with exponential backoff + # * Connection pooling and reuse + # * TLS/SSL support with client certificates + # * Optional Rust backend for enhanced performance (v2.0+) + # * Automatic redirect following + # * Comprehensive error handling + # + # == Basic Usage + # + # # Simple GET request + # response = Net::Hippie.get('https://api.github.com/users/octocat') + # data = JSON.parse(response.body) + # + # # POST with JSON body + # response = Net::Hippie.post('https://httpbin.org/post', + # body: { name: 'hippie', version: '2.0' }) + # + # == Rust Backend (v2.0+) + # + # # Enable high-performance Rust backend + # ENV['NET_HIPPIE_RUST'] = 'true' + # response = Net::Hippie.get('https://api.example.com') # Uses Rust! + # + # @see Client The main client class for advanced usage + # @see https://github.com/xlgmokha/net-hippie Documentation and examples module Hippie + # List of network-related exceptions that should trigger automatic retries. + # These errors typically indicate transient network issues that may resolve + # on subsequent attempts. + # + # @since 0.2.7 CONNECTION_ERRORS = [ EOFError, Errno::ECONNREFUSED, @@ -31,40 +68,126 @@ module Net Timeout::Error ].freeze + # Gets the current logger instance. + # Defaults to a null logger (no output) if not explicitly set. + # + # @return [Logger, nil] The current logger instance + # @since 1.2.0 + # + # @example + # Net::Hippie.logger = Logger.new(STDOUT) + # logger = Net::Hippie.logger def self.logger @logger ||= Logger.new(nil) end + # Sets the logger for HTTP request debugging and error reporting. + # + # @param logger [Logger, nil] Logger instance or nil to disable logging + # @return [Logger, nil] The assigned logger + # @since 1.2.0 + # + # @example Enable debug logging + # Net::Hippie.logger = Logger.new(STDERR) + # Net::Hippie.logger.level = Logger::DEBUG + # + # @example Disable logging + # Net::Hippie.logger = nil def self.logger=(logger) @logger = logger end + # Gets the default SSL verification mode for HTTPS connections. + # + # @return [Integer] OpenSSL verification mode constant + # @since 0.2.3 def self.verify_mode @verify_mode ||= OpenSSL::SSL::VERIFY_PEER end + # Sets the default SSL verification mode for HTTPS connections. + # + # @param mode [Integer] OpenSSL verification mode constant + # @return [Integer] The assigned verification mode + # @since 0.2.3 + # + # @example Disable SSL verification (not recommended for production) + # Net::Hippie.verify_mode = OpenSSL::SSL::VERIFY_NONE def self.verify_mode=(mode) @verify_mode = mode end + # Generates a Basic Authentication header value. + # + # @param username [String] The username for authentication + # @param password [String] The password for authentication + # @return [String] Base64-encoded Basic auth header value + # @since 0.2.1 + # + # @example + # auth_header = Net::Hippie.basic_auth('user', 'pass') + # response = Net::Hippie.get('https://api.example.com', + # headers: { 'Authorization' => auth_header }) def self.basic_auth(username, password) "Basic #{::Base64.strict_encode64("#{username}:#{password}")}" end + # Generates a Bearer Token authentication header value. + # + # @param token [String] The bearer token for authentication + # @return [String] Bearer auth header value + # @since 0.2.1 + # + # @example + # auth_header = Net::Hippie.bearer_auth('your-api-token') + # response = Net::Hippie.get('https://api.example.com', + # headers: { 'Authorization' => auth_header }) def self.bearer_auth(token) "Bearer #{token}" end + # Delegates HTTP method calls to the default client with automatic retry. + # Supports all HTTP methods available on the Client class (get, post, put, etc.). + # + # @param symbol [Symbol] The HTTP method name to call + # @param args [Array] Arguments to pass to the HTTP method + # @return [Net::HTTPResponse] The HTTP response from the request + # @raise [Net::ReadTimeout, Net::OpenTimeout] When request times out + # @raise [Errno::ECONNREFUSED] When connection is refused + # @since 1.0.0 + # + # @example GET request + # response = Net::Hippie.get('https://api.github.com/users/octocat') + # + # @example POST request + # response = Net::Hippie.post('https://httpbin.org/post', body: { key: 'value' }) + # + # @see Client#get, Client#post, Client#put, Client#patch, Client#delete def self.method_missing(symbol, *args) default_client.with_retry(retries: 3) do |client| client.public_send(symbol, *args) end || super end + # Checks if the module responds to HTTP method calls by delegating to Client. + # + # @param name [Symbol] The method name to check + # @param _include_private [Boolean] Whether to include private methods (ignored) + # @return [Boolean] True if the method is supported + # @since 1.0.0 def self.respond_to_missing?(name, _include_private = false) Client.public_instance_methods.include?(name.to_sym) || super end + # Gets the shared default client instance used for module-level HTTP calls. + # The client is configured with automatic redirects and uses the module logger. + # + # @return [Client] The default client instance + # @since 1.0.0 + # + # @example Access the default client directly + # client = Net::Hippie.default_client + # client.get('https://api.example.com') def self.default_client @default_client ||= Client.new(follow_redirects: 3, logger: logger) end diff --git a/lib/net/hippie/client.rb b/lib/net/hippie/client.rb index 340531b..e4bfba8 100644 --- a/lib/net/hippie/client.rb +++ b/lib/net/hippie/client.rb @@ -2,16 +2,93 @@ module Net module Hippie - # A simple client for connecting with http resources. + # HTTP client with connection pooling, automatic retries, and JSON-first defaults. + # + # The Client class provides the core HTTP functionality for Net::Hippie, supporting + # all standard HTTP methods with intelligent defaults for JSON APIs. Features include: + # + # * Connection pooling and reuse per host + # * Automatic retry with exponential backoff + # * Redirect following with configurable limits + # * TLS/SSL support with client certificates + # * Comprehensive timeout configuration + # * Pluggable content-type mapping + # + # @since 0.1.0 + # + # == Basic Usage + # + # client = Net::Hippie::Client.new + # response = client.get('https://api.github.com/users/octocat') + # data = JSON.parse(response.body) + # + # == Advanced Configuration + # + # client = Net::Hippie::Client.new( + # read_timeout: 30, + # open_timeout: 10, + # follow_redirects: 5, + # headers: { 'User-Agent' => 'MyApp/1.0' } + # ) + # + # == Retry Logic + # + # # Automatic retries with exponential backoff + # response = client.with_retry(retries: 3) do |c| + # c.post('https://api.example.com/data', body: payload) + # end + # + # @see Net::Hippie The main module for simple usage class Client + # Default HTTP headers sent with every request. + # Configured for JSON APIs with a descriptive User-Agent. + # + # @since 0.1.0 DEFAULT_HEADERS = { 'Accept' => 'application/json', 'Content-Type' => 'application/json', 'User-Agent' => "net/hippie #{Net::Hippie::VERSION}" }.freeze + # @!attribute [r] mapper + # @return [ContentTypeMapper] Content type mapper for request bodies + # @!attribute [r] logger + # @return [Logger, nil] Logger instance for debugging + # @!attribute [r] follow_redirects + # @return [Integer] Maximum number of redirects to follow attr_reader :mapper, :logger, :follow_redirects + # Creates a new HTTP client with optional configuration. + # + # @param options [Hash] Client configuration options + # @option options [ContentTypeMapper] :mapper Custom content-type mapper + # @option options [Logger, nil] :logger Logger for request debugging + # @option options [Integer] :follow_redirects Maximum redirects to follow (default: 0) + # @option options [Hash] :headers Default headers to merge with requests + # @option options [Integer] :read_timeout Socket read timeout in seconds (default: 10) + # @option options [Integer] :open_timeout Socket open timeout in seconds (default: 10) + # @option options [Integer] :verify_mode SSL verification mode (default: VERIFY_PEER) + # @option options [String] :certificate Client certificate for mutual TLS + # @option options [String] :key Private key for client certificate + # @option options [String] :passphrase Passphrase for encrypted private key + # + # @since 0.1.0 + # + # @example Basic client + # client = Net::Hippie::Client.new + # + # @example Client with custom timeouts + # client = Net::Hippie::Client.new( + # read_timeout: 30, + # open_timeout: 5 + # ) + # + # @example Client with mutual TLS + # client = Net::Hippie::Client.new( + # certificate: File.read('client.crt'), + # key: File.read('client.key'), + # passphrase: 'secret' + # ) def initialize(options = {}) @options = options @mapper = options.fetch(:mapper, ContentTypeMapper.new) @@ -24,6 +101,17 @@ module Net end end + # Executes an HTTP request with automatic redirect following. + # + # @param uri [String, URI] The target URI for the request + # @param request [Net::HTTPRequest] The prepared HTTP request object + # @param limit [Integer] Maximum number of redirects to follow + # @yield [request, response] Optional block to process request/response + # @yieldparam request [Net::HTTPRequest] The HTTP request object + # @yieldparam response [Net::HTTPResponse] The HTTP response object + # @return [Net::HTTPResponse] The final HTTP response + # @raise [Net::ReadTimeout, Net::OpenTimeout] When request times out + # @since 0.1.0 def execute(uri, request, limit: follow_redirects, &block) connection = connection_for(uri) response = connection.run(request) @@ -36,34 +124,118 @@ module Net end end + # Performs an HTTP GET request. + # + # @param uri [String, URI] The target URI + # @param headers [Hash] Additional HTTP headers + # @param body [Hash, String] Request body (typically unused for GET) + # @yield [request, response] Optional block to process request/response + # @return [Net::HTTPResponse] The HTTP response + # @since 0.1.0 + # + # @example Simple GET + # response = client.get('https://api.github.com/users/octocat') + # + # @example GET with custom headers + # response = client.get('https://api.example.com', + # headers: { 'Authorization' => 'Bearer token' }) def get(uri, headers: {}, body: {}, &block) run(uri, Net::HTTP::Get, headers, body, &block) end + # Performs an HTTP PATCH request. + # + # @param uri [String, URI] The target URI + # @param headers [Hash] Additional HTTP headers + # @param body [Hash, String] Request body data + # @yield [request, response] Optional block to process request/response + # @return [Net::HTTPResponse] The HTTP response + # @since 0.2.6 + # + # @example Update resource + # response = client.patch('https://api.example.com/users/123', + # body: { name: 'Updated Name' }) def patch(uri, headers: {}, body: {}, &block) run(uri, Net::HTTP::Patch, headers, body, &block) end + # Performs an HTTP POST request. + # + # @param uri [String, URI] The target URI + # @param headers [Hash] Additional HTTP headers + # @param body [Hash, String] Request body data + # @yield [request, response] Optional block to process request/response + # @return [Net::HTTPResponse] The HTTP response + # @since 0.1.0 + # + # @example Create resource + # response = client.post('https://api.example.com/users', + # body: { name: 'John', email: 'john@example.com' }) def post(uri, headers: {}, body: {}, &block) run(uri, Net::HTTP::Post, headers, body, &block) end + # Performs an HTTP PUT request. + # + # @param uri [String, URI] The target URI + # @param headers [Hash] Additional HTTP headers + # @param body [Hash, String] Request body data + # @yield [request, response] Optional block to process request/response + # @return [Net::HTTPResponse] The HTTP response + # @since 0.1.0 + # + # @example Replace resource + # response = client.put('https://api.example.com/users/123', + # body: { name: 'John', email: 'john@example.com' }) def put(uri, headers: {}, body: {}, &block) run(uri, Net::HTTP::Put, headers, body, &block) end + # Performs an HTTP DELETE request. + # + # @param uri [String, URI] The target URI + # @param headers [Hash] Additional HTTP headers + # @param body [Hash, String] Request body (typically unused for DELETE) + # @yield [request, response] Optional block to process request/response + # @return [Net::HTTPResponse] The HTTP response + # @since 0.1.8 + # + # @example Delete resource + # response = client.delete('https://api.example.com/users/123') def delete(uri, headers: {}, body: {}, &block) run(uri, Net::HTTP::Delete, headers, body, &block) end - # attempt 1 -> delay 0.1 second - # attempt 2 -> delay 0.2 second - # attempt 3 -> delay 0.4 second - # attempt 4 -> delay 0.8 second - # attempt 5 -> delay 1.6 second - # attempt 6 -> delay 3.2 second - # attempt 7 -> delay 6.4 second - # attempt 8 -> delay 12.8 second + # Executes HTTP requests with automatic retry and exponential backoff. + # + # Retry logic with exponential backoff and jitter: + # * Attempt 1 -> delay 0.1 second + # * Attempt 2 -> delay 0.2 second + # * Attempt 3 -> delay 0.4 second + # * Attempt 4 -> delay 0.8 second + # * Attempt 5 -> delay 1.6 second + # * Attempt 6 -> delay 3.2 second + # * Attempt 7 -> delay 6.4 second + # * Attempt 8 -> delay 12.8 second + # + # Only retries on network-related errors defined in CONNECTION_ERRORS. + # + # @param retries [Integer] Maximum number of retry attempts (default: 3) + # @yield [client] Block that performs the HTTP request + # @yieldparam client [Client] The client instance to use for requests + # @return [Net::HTTPResponse] The successful HTTP response + # @raise [Net::ReadTimeout, Net::OpenTimeout] When all retry attempts fail + # @since 0.2.1 + # + # @example Retry a POST request + # response = client.with_retry(retries: 5) do |c| + # c.post('https://api.unreliable.com/data', body: payload) + # end + # + # @example No retries + # response = client.with_retry(retries: 0) do |c| + # c.get('https://api.example.com/health') + # end def with_retry(retries: 3) retries = 0 if retries.nil? || retries.negative? diff --git a/lib/net/hippie/connection.rb b/lib/net/hippie/connection.rb index 83097ff..73b5d84 100644 --- a/lib/net/hippie/connection.rb +++ b/lib/net/hippie/connection.rb @@ -4,8 +4,45 @@ require_relative 'rust_backend' module Net module Hippie - # A connection to a specific host + # Connection abstraction layer that supports both Ruby and Rust backends. + # + # The Connection class provides a unified interface for HTTP connections, + # automatically selecting between the Ruby implementation and the optional + # high-performance Rust backend based on availability and configuration. + # + # Backend selection logic: + # 1. If NET_HIPPIE_RUST=true and Rust extension available -> RustConnection + # 2. Otherwise -> RubyConnection (classic net/http implementation) + # + # @since 0.1.0 + # @since 2.0.0 Added Rust backend support + # + # == Backend Switching + # + # # Enable Rust backend (requires compilation) + # ENV['NET_HIPPIE_RUST'] = 'true' + # connection = Net::Hippie::Connection.new('https', 'api.example.com', 443) + # # Uses RustConnection if available, falls back to RubyConnection + # + # @see RubyConnection The Ruby/net-http implementation + # @see RustConnection The optional Rust implementation class Connection + # Creates a new connection with automatic backend selection. + # + # @param scheme [String] URL scheme ('http' or 'https') + # @param host [String] Target hostname + # @param port [Integer] Target port number + # @param options [Hash] Connection configuration options + # @option options [Integer] :read_timeout Socket read timeout in seconds + # @option options [Integer] :open_timeout Socket connection timeout in seconds + # @option options [Integer] :verify_mode SSL verification mode + # @option options [String] :certificate Client certificate for mutual TLS + # @option options [String] :key Private key for client certificate + # @option options [String] :passphrase Passphrase for encrypted private key + # @option options [Logger] :logger Logger for connection debugging + # + # @since 0.1.0 + # @since 2.0.0 Added automatic backend selection def initialize(scheme, host, port, options = {}) @scheme = scheme @host = host @@ -20,16 +57,35 @@ module Net end end + # Executes an HTTP request using the selected backend. + # + # @param request [Net::HTTPRequest] The HTTP request to execute + # @return [Net::HTTPResponse] The HTTP response + # @raise [Net::ReadTimeout, Net::OpenTimeout] When request times out + # @since 0.1.0 def run(request) @backend.run(request) end + # Builds a complete URL from a path, handling absolute and relative URLs. + # + # @param path [String] URL path (absolute or relative) + # @return [String] Complete URL + # @since 0.1.0 def build_url_for(path) @backend.build_url_for(path) end private + # Creates the Ruby backend implementation. + # + # @param scheme [String] URL scheme + # @param host [String] Target hostname + # @param port [Integer] Target port + # @param options [Hash] Connection options + # @return [RubyConnection] Ruby backend instance + # @since 2.0.0 def create_ruby_backend(scheme, host, port, options) # This is the original Ruby implementation wrapped in an object # that matches the same interface as RustConnection @@ -37,8 +93,31 @@ module Net end end - # Wrapper for the original Ruby implementation + # Ruby implementation of HTTP connections using net/http. + # + # This class provides the traditional net/http-based HTTP client functionality + # that has been the backbone of Net::Hippie since its inception. It supports + # all standard HTTP features including SSL/TLS, client certificates, and + # comprehensive timeout configuration. + # + # @since 2.0.0 Extracted from Connection class + # @see Connection The main connection interface class RubyConnection + # Creates a new Ruby HTTP connection using net/http. + # + # @param scheme [String] URL scheme ('http' or 'https') + # @param host [String] Target hostname + # @param port [Integer] Target port number + # @param options [Hash] Connection configuration options + # @option options [Integer] :read_timeout Socket read timeout (default: 10) + # @option options [Integer] :open_timeout Socket connection timeout (default: 10) + # @option options [Integer] :verify_mode SSL verification mode + # @option options [String] :certificate Client certificate for mutual TLS + # @option options [String] :key Private key for client certificate + # @option options [String] :passphrase Passphrase for encrypted private key + # @option options [Logger] :logger Logger for connection debugging + # + # @since 2.0.0 def initialize(scheme, host, port, options = {}) @scheme = scheme @host = host @@ -54,10 +133,30 @@ module Net @http = http end + # Executes an HTTP request using net/http. + # + # @param request [Net::HTTPRequest] The HTTP request to execute + # @return [Net::HTTPResponse] The HTTP response + # @raise [Net::ReadTimeout] When read timeout expires + # @raise [Net::OpenTimeout] When connection timeout expires + # @since 2.0.0 def run(request) @http.request(request) end + # Builds a complete URL from a path. + # + # @param path [String] URL path (absolute URLs returned as-is) + # @return [String] Complete URL with scheme, host, and path + # @since 2.0.0 + # + # @example Relative path + # connection.build_url_for('/api/users') + # # => "https://api.example.com/api/users" + # + # @example Absolute URL + # connection.build_url_for('https://other.com/path') + # # => "https://other.com/path" def build_url_for(path) return path if path.start_with?('http') @@ -66,6 +165,14 @@ module Net private + # Applies client TLS certificate configuration to the HTTP connection. + # + # @param http [Net::HTTP] The HTTP connection object + # @param options [Hash] TLS configuration options + # @option options [String] :certificate Client certificate in PEM format + # @option options [String] :key Private key in PEM format + # @option options [String] :passphrase Optional passphrase for encrypted key + # @since 2.0.0 def apply_client_tls_to(http, options) return if options[:certificate].nil? || options[:key].nil? @@ -73,6 +180,13 @@ module Net http.key = private_key(options[:key], options[:passphrase]) end + # Creates a private key object from PEM data. + # + # @param key [String] Private key in PEM format + # @param passphrase [String, nil] Optional passphrase for encrypted keys + # @param type [Class] OpenSSL key class (default: RSA) + # @return [OpenSSL::PKey] Private key object + # @since 2.0.0 def private_key(key, passphrase, type = OpenSSL::PKey::RSA) passphrase ? type.new(key, passphrase) : type.new(key) end diff --git a/lib/net/hippie/content_type_mapper.rb b/lib/net/hippie/content_type_mapper.rb index 76cf437..9a9dd0e 100644 --- a/lib/net/hippie/content_type_mapper.rb +++ b/lib/net/hippie/content_type_mapper.rb @@ -2,8 +2,74 @@ module Net module Hippie - # Converts a ruby hash into a JSON string + # Content-type aware request body serialization. + # + # The ContentTypeMapper handles automatic serialization of request bodies + # based on the Content-Type header. It provides intelligent defaults for + # JSON APIs while supporting custom serialization strategies. + # + # == Default Behavior + # + # * JSON content types -> Automatic JSON.generate() serialization + # * String bodies -> Passed through unchanged + # * Other content types -> No transformation (body as-is) + # + # @since 0.1.0 + # + # == Usage + # + # mapper = Net::Hippie::ContentTypeMapper.new + # + # # JSON serialization + # json_body = mapper.map_from( + # { 'Content-Type' => 'application/json' }, + # { name: 'Alice', age: 30 } + # ) + # # => '{"name":"Alice","age":30}' + # + # # String pass-through + # xml_body = mapper.map_from( + # { 'Content-Type' => 'application/xml' }, + # '<user><name>Alice</name></user>' + # ) + # # => '<user><name>Alice</name></user>' + # + # @see Client#initialize The :mapper option for custom mappers class ContentTypeMapper + # Maps request body data based on Content-Type header. + # + # Performs automatic serialization for known content types: + # * application/json -> JSON.generate() + # * application/*+json -> JSON.generate() + # * String bodies -> No transformation + # * Other types -> No transformation + # + # @param headers [Hash] HTTP headers (must include 'Content-Type') + # @param body [Object] Request body data to serialize + # @return [String, Object] Serialized body or original object + # @since 0.1.0 + # + # @example JSON serialization + # mapper = ContentTypeMapper.new + # result = mapper.map_from( + # { 'Content-Type' => 'application/json' }, + # { user: { name: 'Alice', email: 'alice@example.com' } } + # ) + # # => '{"user":{"name":"Alice","email":"alice@example.com"}}' + # + # @example String pass-through + # result = mapper.map_from( + # { 'Content-Type' => 'text/plain' }, + # 'Hello, World!' + # ) + # # => 'Hello, World!' + # + # @example Custom JSON content type + # result = mapper.map_from( + # { 'Content-Type' => 'application/vnd.api+json' }, + # { data: { type: 'users', attributes: { name: 'Alice' } } } + # ) + # # => '{"data":{"type":"users","attributes":{"name":"Alice"}}}' def map_from(headers, body) return body if body.is_a?(String) diff --git a/lib/net/hippie/rust_backend.rb b/lib/net/hippie/rust_backend.rb index 7d1637e..d25abf7 100644 --- a/lib/net/hippie/rust_backend.rb +++ b/lib/net/hippie/rust_backend.rb @@ -2,10 +2,57 @@ module Net module Hippie - # Rust backend integration + # Rust backend integration and availability detection. + # + # The RustBackend module manages the optional high-performance Rust HTTP client + # backend. It provides automatic detection of Rust extension availability and + # environment-based enabling/disabling of the Rust backend. + # + # == Backend Selection Logic + # + # 1. Check if NET_HIPPIE_RUST environment variable is set to 'true' + # 2. Verify that the Rust extension (net_hippie_ext) can be loaded + # 3. If both conditions are met, use RustConnection + # 4. Otherwise, fall back to RubyConnection + # + # == Performance Benefits + # + # When enabled, the Rust backend provides: + # * Significantly faster HTTP requests using reqwest + # * Better concurrency with Tokio async runtime + # * Lower memory usage with zero-cost abstractions + # * Type safety with compile-time guarantees + # + # @since 2.0.0 + # + # == Environment Configuration + # + # # Enable Rust backend + # ENV['NET_HIPPIE_RUST'] = 'true' + # + # # Check availability and status + # puts "Rust available: #{Net::Hippie::RustBackend.available?}" + # puts "Rust enabled: #{Net::Hippie::RustBackend.enabled?}" + # + # @see RUST_BACKEND.md Detailed setup and usage documentation module RustBackend @rust_available = nil + # Checks if the Rust extension is available for loading. + # + # This method attempts to require the 'net_hippie_ext' native extension + # and caches the result. The extension is built from Rust source code + # using Magnus for Ruby-Rust integration. + # + # @return [Boolean] true if Rust extension loaded successfully + # @since 2.0.0 + # + # @example Check Rust availability + # if Net::Hippie::RustBackend.available? + # puts "Rust backend ready!" + # else + # puts "Using Ruby backend (Rust not available)" + # end def self.available? return @rust_available unless @rust_available.nil? @@ -17,30 +64,102 @@ module Net end end + # Checks if the Rust backend is both available and enabled. + # + # Returns true only when: + # 1. NET_HIPPIE_RUST environment variable is set to 'true' + # 2. The Rust extension is available (compiled and loadable) + # + # @return [Boolean] true if Rust backend should be used + # @since 2.0.0 + # + # @example Check if Rust backend will be used + # ENV['NET_HIPPIE_RUST'] = 'true' + # if Net::Hippie::RustBackend.enabled? + # puts "All HTTP requests will use Rust backend" + # else + # puts "Falling back to Ruby backend" + # end def self.enabled? ENV['NET_HIPPIE_RUST'] == 'true' && available? end - # Adapter to make RustResponse behave like Net::HTTPResponse + # Adapter that makes Rust HTTP responses compatible with Net::HTTPResponse interface. + # + # The ResponseAdapter provides a compatibility layer between Rust HTTP responses + # and Ruby's Net::HTTPResponse objects. This ensures that existing code works + # unchanged when switching between Ruby and Rust backends. + # + # == Compatibility Features + # + # * Status code access via #code method + # * Response body access via #body method + # * Header access via #[] method + # * Response class detection via #class method + # * Type checking via #is_a? and #kind_of? + # + # @since 2.0.0 + # + # == Supported Response Classes + # + # * Net::HTTPOK (200) + # * Net::HTTPCreated (201) + # * Net::HTTPRedirection (3xx) + # * Net::HTTPClientError (4xx) + # * Net::HTTPServerError (5xx) + # + # @see Net::HTTPResponse The Ruby standard library response interface class ResponseAdapter + # Creates a new response adapter from a Rust HTTP response. + # + # @param rust_response [RustResponse] The Rust HTTP response object + # @since 2.0.0 def initialize(rust_response) @rust_response = rust_response @code = rust_response.code @body = rust_response.body end + # Returns the HTTP status code. + # + # @return [String] HTTP status code (e.g., "200", "404") + # @since 2.0.0 def code @code end + # Returns the response body content. + # + # @return [String] HTTP response body + # @since 2.0.0 def body @body end + # Retrieves a response header value by name. + # + # @param header_name [String, Symbol] Header name (case-insensitive) + # @return [String, nil] Header value or nil if not found + # @since 2.0.0 + # + # @example Get content type + # content_type = response['Content-Type'] + # location = response[:location] def [](header_name) @rust_response[header_name.to_s] end + # Returns the appropriate Net::HTTP response class based on status code. + # + # Maps HTTP status codes to their corresponding Net::HTTP class constants + # to maintain compatibility with Ruby HTTP library expectations. + # + # @return [Class] Net::HTTP response class constant + # @since 2.0.0 + # + # @example Check response type + # response.class # => Net::HTTPOK (for 200 status) + # response.class # => Net::HTTPNotFound (for 404 status) def class case @code.to_i when 200 @@ -58,11 +177,27 @@ module Net end end - # Make it behave like the expected response class + # Checks if this response is an instance of the given class. + # + # Provides compatibility with Ruby's type checking by delegating + # to the mapped response class while supporting normal inheritance. + # + # @param klass [Class] Class to check against + # @return [Boolean] true if response matches the class + # @since 2.0.0 + # + # @example Type checking + # response.is_a?(Net::HTTPOK) # => true (for 200 status) + # response.is_a?(Net::HTTPRedirection) # => true (for 3xx status) def is_a?(klass) self.class == klass || super end + # Alias for #is_a? to maintain Ruby compatibility. + # + # @param klass [Class] Class to check against + # @return [Boolean] true if response matches the class + # @since 2.0.0 def kind_of?(klass) is_a?(klass) end |
