Command Palette
Search for a command to run...
Ruby
Ruby SDK with elegant syntax and Rails integration
Installation & Setup
Required dependencies and setup for Ruby development
Ruby & Bundler Setup
# Install Ruby (using rbenv)
rbenv install 3.2.0
rbenv global 3.2.0
gem install bundler
bundle install
export LEAKZERO_API_KEY=your_api_key_here
ruby basic_usage.rbGemfile
# Gemfile
source 'https://rubygems.org'
ruby '~> 3.2.0'
gem 'concurrent-ruby', '~> 1.2'
gem 'logger', '~> 1.5'
# Development dependencies
group :development, :test do
gem 'rspec', '~> 3.12'
gem 'rubocop', '~> 1.50'
gem 'rubocop-rspec', '~> 2.20'
gem 'pry', '~> 0.14'
gem 'yard', '~> 0.9'
end
# Optional dependencies
gem 'dotenv', '~> 2.8' # For environment variables
gem 'httparty', '~> 0.21' # Alternative HTTP client
gem 'redis', '~> 5.0' # For caching (optional).env
LEAKZERO_API_KEY=your_api_key_here
LEAKZERO_BASE_URL=https://api.leakzero.io/api/v1
LOG_LEVEL=infoRuby Version: This example requires Ruby 3.0+ for modern features like pattern matching and improved performance. Uses concurrent-ruby for thread-safe operations.
Quick Start Example
Simple example to get you started with Ruby
basic_usage.rb
#!/usr/bin/env ruby
# frozen_string_literal: true
require_relative 'leakzero_client'
def basic_search
client = LeakZero::Client.new(ENV['LEAKZERO_API_KEY'])
begin
options = LeakZero::SearchOptions.new('email', '[email protected]')
options.row_limit = 100
result = client.regular_search(options)
puts "Found #{result.total_results} records"
puts "Cost: $#{'%.6f' % result.cost}"
result.data.first(5).each do |record|
breach = record['breach_name'] || 'Unknown'
date = record['breach_date'] || 'Unknown'
puts "Breach: #{breach}, Date: #{date}"
end
analysis = client.analyze_breaches(result)
puts "\nUnique breaches: #{analysis[:unique_breaches]}"
rescue => e
puts "Search failed: #{e.message}"
end
end
basic_search if __FILE__ == $0Production ReadyComplete Ruby Implementation
Full-featured client with thread safety, concurrent operations, and Ruby idioms
leakzero_client.rb
#!/usr/bin/env ruby
require 'net/http'
require 'uri'
require 'json'
require 'logger'
require 'time'
require 'securerandom'
require 'concurrent'
module LeakZero
class SearchOptions
attr_accessor :field, :value, :option, :row_limit, :auto_export
def initialize(field, value)
@field = field
@value = value
@option = 'exact'
@row_limit = 1000
@auto_export = nil
end
def to_hash
hash = {
field: @field,
value: @value,
option: @option,
rowLimit: @row_limit
}
hash[:autoExport] = @auto_export.to_json if @auto_export
hash
end
end
class SearchResult
attr_accessor :data, :total_results, :cost, :request_id, :processing_time, :analysis
def initialize
@data = []
@total_results = 0
@cost = 0.0
@request_id = ''
@processing_time = 0
@analysis = {}
end
def add_record(record)
@data << record
end
def size
@data.size
end
def empty?
@data.empty?
end
end
class BalanceResponse
attr_accessor :balance, :currency, :last_usage
def initialize(balance: 0.0, currency: 'USD', last_usage: '')
@balance = balance
@currency = currency
@last_usage = last_usage
end
def self.from_hash(hash)
new(
balance: hash['balance'] || 0.0,
currency: hash['currency'] || 'USD',
last_usage: hash['lastUsage'] || ''
)
end
end
class UsageStats
attr_accessor :total_requests, :total_cost, :requests_today, :cost_today,
:requests_this_week, :cost_this_week
def initialize
@total_requests = 0
@total_cost = 0.0
@requests_today = 0
@cost_today = 0.0
@requests_this_week = 0
@cost_this_week = 0.0
end
def self.from_hash(hash)
stats = new
stats.total_requests = hash['totalRequests'] || 0
stats.total_cost = hash['totalCost'] || 0.0
stats.requests_today = hash['requestsToday'] || 0
stats.cost_today = hash['costToday'] || 0.0
stats.requests_this_week = hash['requestsThisWeek'] || 0
stats.cost_this_week = hash['costThisWeek'] || 0.0
stats
end
end
class ApiError < StandardError
attr_reader :error_code, :status_code
def initialize(error_code, message, status_code)
@error_code = error_code
@status_code = status_code
super("API Error [#{status_code}]: #{error_code} - #{message}")
end
end
class HttpClient
def initialize(timeout: 120)
@timeout = timeout
@logger = Logger.new(STDOUT)
@logger.level = Logger::INFO
end
def get(uri, headers = {})
make_request('GET', uri, nil, headers)
end
def post(uri, data, headers = {})
make_request('POST', uri, data, headers)
end
private
def make_request(method, uri, data, headers)
uri = URI(uri) unless uri.is_a?(URI)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = uri.scheme == 'https'
http.read_timeout = @timeout
http.open_timeout = 30
request = case method.upcase
when 'GET'
Net::HTTP::Get.new(uri)
when 'POST'
req = Net::HTTP::Post.new(uri)
req.body = data.is_a?(String) ? data : data.to_json
req
end
headers.each { |key, value| request[key] = value }
response = http.request(request)
case response
when Net::HTTPSuccess
response
else
handle_error_response(response)
end
rescue Net::TimeoutError, Net::OpenTimeout => e
raise StandardError, "Request timeout: #{e.message}"
rescue StandardError => e
raise StandardError, "Request failed: #{e.message}"
end
def handle_error_response(response)
begin
error_data = JSON.parse(response.body)
error_code = error_data['error'] || 'HTTP_ERROR'
error_message = error_data['message'] || "HTTP #{response.code}"
rescue JSON::ParserError
error_code = 'HTTP_ERROR'
error_message = "HTTP #{response.code}: #{response.message}"
end
raise ApiError.new(error_code, error_message, response.code.to_i)
end
end
class Client
attr_reader :api_key, :base_url, :max_retries, :timeout
def initialize(api_key, base_url: 'https://api.leakzero.io/api/v1', max_retries: 3, timeout: 120)
raise ArgumentError, 'API key is required' if api_key.nil? || api_key.empty?
@api_key = api_key
@base_url = base_url.chomp('/')
@max_retries = max_retries
@timeout = timeout
@http_client = HttpClient.new(timeout: timeout)
@logger = Logger.new(STDOUT)
@logger.level = Logger::INFO
end
def get_balance
begin
response = make_request_with_retry('GET', '/public/balance')
data = JSON.parse(response.body)
BalanceResponse.from_hash(data)
rescue => e
@logger.error "Failed to get balance: #{e.message}"
raise
end
end
def regular_search(options)
options.row_limit = [options.row_limit, 10000].min
begin
@logger.info "Searching for #{options.field}: #{options.value}"
response = make_request_with_retry('POST', '/public/search/regular', options.to_hash)
result = SearchResult.new
result.total_results = response['x-rows-returned'].to_i
result.cost = response['x-charge'].to_i / 1_000_000_000.0
result.request_id = response['x-request-id'] || ''
result.processing_time = response['x-processing-time'].to_i
data = JSON.parse(response.body)
if data['data'].is_a?(Array)
data['data'].each { |record| result.add_record(record) }
end
@logger.info "Found #{result.total_results} records (Cost: $#{'%.6f' % result.cost})"
result
rescue => e
@logger.error "Regular search failed: #{e.message}"
raise
end
end
def advanced_search(queries, row_limit: 1000)
raise ArgumentError, 'At least one query is required' if queries.empty?
row_limit = [row_limit, 10000].min
begin
payload = {
queries: queries.map(&:to_hash),
rowLimit: row_limit
}
@logger.info "Advanced search with #{queries.size} queries"
response = make_request_with_retry('POST', '/public/search/advanced', payload)
result = SearchResult.new
result.total_results = response['x-rows-returned'].to_i
result.cost = response['x-charge'].to_i / 1_000_000_000.0
result.request_id = response['x-request-id'] || ''
result.processing_time = response['x-processing-time'].to_i
data = JSON.parse(response.body)
if data['data'].is_a?(Array)
data['data'].each { |record| result.add_record(record) }
end
@logger.info "Advanced search completed (Cost: $#{'%.6f' % result.cost})"
result
rescue => e
@logger.error "Advanced search failed: #{e.message}"
raise
end
end
def identity_linking_search(field, value, context_domain: nil, max_depth: 2, min_confidence: 0.35, max_results: 50, include_possible: true)
max_results = [max_results, 100].min
begin
payload = {
field: field,
value: value,
maxDepth: [max_depth, 4].min,
minConfidence: min_confidence,
maxResults: max_results,
includePossible: include_possible,
contextDomain: context_domain
}
@logger.info "identity linking search for: #{field}:#{value}"
response = make_request_with_retry('POST', '/public/search/identity-linking', payload)
data = JSON.parse(response.body)
result = SearchResult.new
result.total_results = response['x-rows-returned'].to_i
result.cost = response['x-charge'].to_i / 1_000_000_000.0
result.request_id = response['x-request-id'] || ''
result.processing_time = response['x-processing-time'].to_i
correlation = data['data'] || {}
result.analysis['summary'] = correlation['summary'] || {}
result.analysis['metadata'] = correlation['metadata'] || {}
if correlation['results'].is_a?(Array)
correlation['results'].each { |candidate| result.add_record(candidate) }
end
@logger.info "identity linking found #{result.total_results} candidates"
result
rescue => e
@logger.error "Identity linking search failed: #{e.message}"
raise
end
end
def get_usage_stats
begin
response = make_request_with_retry('GET', '/public/usage/stats')
data = JSON.parse(response.body)
UsageStats.from_hash(data)
rescue => e
@logger.error "Failed to get usage stats: #{e.message}"
raise
end
end
def get_search_history(limit: 50, offset: 0)
begin
limit = [limit, 100].min
endpoint = "/public/search-history?limit=#{limit}&offset=#{offset}"
response = make_request_with_retry('GET', endpoint)
JSON.parse(response.body)
rescue => e
@logger.error "Failed to get search history: #{e.message}"
raise
end
end
def analyze_breaches(result)
return { error: 'No records to analyze' } if result.empty?
breach_distribution = Hash.new(0)
date_distribution = Hash.new(0)
field_distribution = Hash.new(0)
result.data.each do |record|
breach_name = record['breach_name'] || 'Unknown'
breach_distribution[breach_name] += 1
breach_date = record['breach_date']
if breach_date && breach_date.length >= 4
year = breach_date[0, 4]
date_distribution[year] += 1 if year.match?(/^d{4}$/)
end
record.each do |field_name, field_value|
if !field_value.nil? && !field_value.to_s.empty? &&
!%w[breach_name breach_date].include?(field_name)
field_distribution[field_name] += 1
end
end
end
years = date_distribution.keys.map(&:to_i)
date_range = {
earliest: years.empty? ? nil : years.min,
latest: years.empty? ? nil : years.max
}
top_breaches = breach_distribution.sort_by { |_, count| -count }.first(5)
.map { |name, count| { name: name, count: count } }
{
total_records: result.size,
unique_breaches: breach_distribution.size,
breach_distribution: breach_distribution.sort_by { |_, count| -count }.first(10).to_h,
date_distribution: date_distribution,
field_distribution: field_distribution,
date_range: date_range,
top_breaches: top_breaches
}
end
def bulk_search(search_terms, field: 'email', batch_size: 5, delay: 0.2, row_limit: 100)
results = []
total_terms = search_terms.size
@logger.info "Starting bulk search for #{total_terms} terms"
search_terms.each_slice(batch_size).with_index do |batch, i|
batch_results = []
batch.each_with_index do |term, j|
begin
sleep(delay) if j > 0
options = SearchOptions.new(field, term)
options.row_limit = row_limit
result = regular_search(options)
batch_results << {
term: term,
result: result,
success: true
}
rescue => e
@logger.error "Failed to search for #{term}: #{e.message}"
batch_results << {
term: term,
error: e.message,
success: false
}
end
end
results.concat(batch_results)
processed = [(i + 1) * batch_size, total_terms].min
@logger.info "Processed #{processed}/#{total_terms} terms"
sleep(delay * 2) if (i + 1) * batch_size < total_terms
end
successful = results.select { |r| r[:success] }
failed = results.select { |r| !r[:success] }
success_rate = results.empty? ? 0 : (successful.size.to_f / results.size * 100)
@logger.info "Bulk search completed: #{successful.size} successful, #{failed.size} failed"
{
successful: successful,
failed: failed,
total_processed: results.size,
success_rate: "#{'%.2f' % success_rate}%"
}
end
def concurrent_search(search_terms, field: 'email', thread_count: 5, row_limit: 50)
thread_pool = Concurrent::FixedThreadPool.new(thread_count)
futures = []
results = Concurrent::Array.new
@logger.info "Starting concurrent search for #{search_terms.size} terms"
search_terms.each_with_index do |term, index|
future = Concurrent::Future.execute(executor: thread_pool) do
sleep(0.2 * (index % thread_count))
options = SearchOptions.new(field, term)
options.row_limit = row_limit
result = regular_search(options)
{
term: term,
result: result,
success: true
}
rescue => e
@logger.error "Concurrent search failed for #{term}: #{e.message}"
{
term: term,
error: e.message,
success: false
}
end
futures << future
end
futures.each { |future| results << future.value }
thread_pool.shutdown
successful = results.select { |r| r[:success] }
@logger.info "Completed #{successful.size} concurrent searches"
results.to_a
end
private
def generate_headers
{
'x-api-key' => @api_key,
'x-timestamp' => Time.now.utc.iso8601,
'x-request-id' => generate_request_id,
'Content-Type' => 'application/json',
'User-Agent' => 'LeakZero-Ruby-SDK/1.0.0'
}
end
def generate_request_id
"req_#{Time.now.to_i}_#{SecureRandom.hex(4)}"
end
def make_request_with_retry(method, endpoint, payload = nil)
url = @base_url + endpoint
headers = generate_headers
(0..@max_retries).each do |attempt|
begin
response = case method.upcase
when 'GET'
@http_client.get(url, headers)
when 'POST'
@http_client.post(url, payload, headers)
end
if response.code == '429'
retry_after = response['Retry-After']&.to_i || 5
retry_after = [retry_after, 30].min # Cap at 30 seconds
@logger.warn "Rate limited. Waiting #{retry_after}s"
sleep(retry_after)
next
end
@logger.info "API Request successful: #{method.upcase} #{endpoint}"
return response
rescue ApiError => e
raise if attempt == @max_retries
@logger.warn "Request attempt #{attempt + 1} failed: #{e.message}"
sleep(attempt + 1)
end
end
raise StandardError, 'Max retries exceeded'
end
end
end
def demonstrate_basic_usage
api_key = ENV['LEAKZERO_API_KEY']
raise 'LEAKZERO_API_KEY environment variable is required' if api_key.nil?
begin
client = LeakZero::Client.new(api_key, max_retries: 5)
puts 'Checking API balance...'
balance = client.get_balance
puts "Current balance: $#{'%.6f' % balance.balance}"
if balance.balance < 0.001
raise 'Insufficient balance. Please top up your account.'
end
puts "
Performing regular search..."
options = LeakZero::SearchOptions.new('email', '[email protected]')
options.option = 'exact'
options.row_limit = 100
result = client.regular_search(options)
puts "Found #{result.total_results} records"
puts "Cost: $#{'%.6f' % result.cost}"
result.data.first(5).each do |record|
breach = record['breach_name'] || 'Unknown'
date = record['breach_date'] || 'Unknown'
puts "Breach: #{breach}, Date: #{date}"
end
unless result.empty?
analysis = client.analyze_breaches(result)
puts "
Analysis: #{analysis[:unique_breaches]} unique breaches"
puts 'Top breaches:'
analysis[:top_breaches].each do |breach|
puts " - #{breach[:name]}: #{breach[:count]} records"
end
end
puts "
Getting usage statistics..."
stats = client.get_usage_stats
puts "Total requests: #{stats.total_requests}"
puts "Total cost: $#{'%.6f' % stats.total_cost}"
rescue => e
puts "Error: #{e.message}"
end
end
def demonstrate_concurrent_usage
api_key = ENV['LEAKZERO_API_KEY']
raise 'LEAKZERO_API_KEY environment variable is required' if api_key.nil?
begin
client = LeakZero::Client.new(api_key)
search_terms = %w[[email protected] [email protected] [email protected]]
results = client.concurrent_search(search_terms, thread_count: 3)
successful = results.select { |r| r[:success] }
puts "Completed #{successful.size} concurrent searches"
rescue => e
puts "Concurrent search error: #{e.message}"
end
end
if __FILE__ == $0
demonstrate_basic_usage
puts "
#{'=' * 50}"
demonstrate_concurrent_usage
endRuby Idioms: Uses Ruby's natural syntax with blocks, symbols, and duck typing
Thread Safety: Concurrent-ruby gem for thread-safe operations and futures
Flexible APIs: Method chaining and keyword arguments for clean interfaces
Key Features
Core Features
- • Idiomatic Ruby with blocks and symbols
- • Thread-safe operations with concurrent-ruby
- • Automatic retry with exponential backoff
- • Rate limit handling with intelligent delays
- • Comprehensive error handling with custom exceptions
Performance Features
- • Thread pool for concurrent HTTP requests
- • Connection keep-alive and timeout management
- • Bulk search operations with batching
- • Future-based asynchronous operations
- • Memory-efficient data processing
Best Practices:
- • Always use environment variables for API keys
- • Implement proper exception handling in production
- • Use thread pools for concurrent operations
- • Monitor your API usage and balance regularly
- • Follow Ruby community style guidelines