lib/turbopuffer/errors.rb



# frozen_string_literal: true

module Turbopuffer
  module Errors
    class Error < StandardError
      # @!attribute cause
      #
      #   @return [StandardError, nil]
    end

    class ConversionError < Turbopuffer::Errors::Error
      # @return [StandardError, nil]
      def cause = @cause.nil? ? super : @cause

      # @api private
      #
      # @param on [Class<StandardError>]
      # @param method [Symbol]
      # @param target [Object]
      # @param value [Object]
      # @param cause [StandardError, nil]
      def initialize(on:, method:, target:, value:, cause: nil)
        cls = on.name.split("::").last

        message = [
          "Failed to parse #{cls}.#{method} from #{value.class} to #{target.inspect}.",
          "To get the unparsed API response, use #{cls}[#{method.inspect}].",
          cause && "Cause: #{cause.message}"
        ].filter(&:itself).join(" ")

        @cause = cause
        super(message)
      end
    end

    class APIError < Turbopuffer::Errors::Error
      # @return [URI::Generic]
      attr_accessor :url

      # @return [Integer, nil]
      attr_accessor :status

      # @return [Object, nil]
      attr_accessor :body

      # @api private
      #
      # @param url [URI::Generic]
      # @param status [Integer, nil]
      # @param body [Object, nil]
      # @param request [nil]
      # @param response [nil]
      # @param message [String, nil]
      def initialize(url:, status: nil, body: nil, request: nil, response: nil, message: nil)
        @url = url
        @status = status
        @body = body
        @request = request
        @response = response
        super(message)
      end
    end

    class APIConnectionError < Turbopuffer::Errors::APIError
      # @!attribute status
      #
      #   @return [nil]

      # @!attribute body
      #
      #   @return [nil]

      # @api private
      #
      # @param url [URI::Generic]
      # @param status [nil]
      # @param body [nil]
      # @param request [nil]
      # @param response [nil]
      # @param message [String, nil]
      def initialize(
        url:,
        status: nil,
        body: nil,
        request: nil,
        response: nil,
        message: "Connection error."
      )
        super
      end
    end

    class APITimeoutError < Turbopuffer::Errors::APIConnectionError
      # @api private
      #
      # @param url [URI::Generic]
      # @param status [nil]
      # @param body [nil]
      # @param request [nil]
      # @param response [nil]
      # @param message [String, nil]
      def initialize(
        url:,
        status: nil,
        body: nil,
        request: nil,
        response: nil,
        message: "Request timed out."
      )
        super
      end
    end

    class APIStatusError < Turbopuffer::Errors::APIError
      # @api private
      #
      # @param url [URI::Generic]
      # @param status [Integer]
      # @param body [Object, nil]
      # @param request [nil]
      # @param response [nil]
      # @param message [String, nil]
      #
      # @return [self]
      def self.for(url:, status:, body:, request:, response:, message: nil)
        kwargs = {
          url: url,
          status: status,
          body: body,
          request: request,
          response: response,
          message: message
        }

        case status
        in 400
          Turbopuffer::Errors::BadRequestError.new(**kwargs)
        in 401
          Turbopuffer::Errors::AuthenticationError.new(**kwargs)
        in 403
          Turbopuffer::Errors::PermissionDeniedError.new(**kwargs)
        in 404
          Turbopuffer::Errors::NotFoundError.new(**kwargs)
        in 409
          Turbopuffer::Errors::ConflictError.new(**kwargs)
        in 422
          Turbopuffer::Errors::UnprocessableEntityError.new(**kwargs)
        in 429
          Turbopuffer::Errors::RateLimitError.new(**kwargs)
        in (500..)
          Turbopuffer::Errors::InternalServerError.new(**kwargs)
        else
          Turbopuffer::Errors::APIStatusError.new(**kwargs)
        end
      end

      # @!parse
      #   # @return [Integer]
      #   attr_accessor :status

      # @api private
      #
      # @param url [URI::Generic]
      # @param status [Integer]
      # @param body [Object, nil]
      # @param request [nil]
      # @param response [nil]
      # @param message [String, nil]
      def initialize(url:, status:, body:, request:, response:, message: nil)
        message ||= {url: url.to_s, status: status, body: body}
        super(
          url: url,
          status: status,
          body: body,
          request: request,
          response: response,
          message: message&.to_s
        )
      end
    end

    class BadRequestError < Turbopuffer::Errors::APIStatusError
      HTTP_STATUS = 400
    end

    class AuthenticationError < Turbopuffer::Errors::APIStatusError
      HTTP_STATUS = 401
    end

    class PermissionDeniedError < Turbopuffer::Errors::APIStatusError
      HTTP_STATUS = 403
    end

    class NotFoundError < Turbopuffer::Errors::APIStatusError
      HTTP_STATUS = 404
    end

    class ConflictError < Turbopuffer::Errors::APIStatusError
      HTTP_STATUS = 409
    end

    class UnprocessableEntityError < Turbopuffer::Errors::APIStatusError
      HTTP_STATUS = 422
    end

    class RateLimitError < Turbopuffer::Errors::APIStatusError
      HTTP_STATUS = 429
    end

    class InternalServerError < Turbopuffer::Errors::APIStatusError
      HTTP_STATUS = (500..)
    end
  end
end