Commit c4ebba0
Changed files (5)
lib
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?
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
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)
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
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