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.rb

Gemfile

# 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=info
Ruby 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__ == $0
Production 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
end
Ruby 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
Documentation - LeakZero | LeakZero