decko-commons/decko

View on GitHub
card/lib/card/error.rb

Summary

Maintainability
A
0 mins
Test Coverage
B
88%
# -*- encoding : utf-8 -*-

class StandardError
  MAX_BACKTRACE_LINES = 20

  def report
    Rails.logger.info "exception = #{self.class}: #{message}"
    Rails.logger.debug backtrace[0..MAX_BACKTRACE_LINES].join("\n")
  end
end

class Card
  # exceptions and errors.
  # (arguably most of these should be Card::Exception)
  class Error < StandardError
    cattr_accessor :current
    class_attribute :status_code, :view
    attr_writer :backtrace

    self.view = :errors
    self.status_code = 422

    attr_accessor :card

    def initialize message=nil
      if message.is_a? Card
        self.card = message
        message = message_from_card
      end
      super message
    end

    def message_from_card
      ::I18n.t :lib_exception_for_card, cardname: card.name, message: card_message_text
    end

    def backtrace
      @backtrace || super
    end

    def card_message_text
      card.errors.first&.message
    end

    # error attributable to code (as opposed to card configuration)
    class ServerError < Error
      def self.view
        debugger_on? ? :debug_server_error : :server_error
      end

      def self.status_code
        # Errors with status code 900 are displayed as modal instead of inside
        # the "card-notice" div``
        debugger_on? ? 900 : 500
      end

      def self.debugger_on?
        Card::Codename[:debugger] && Card[:debugger]&.content =~ /on/
      end

      def report
        super
        card&.notable_exception_raised
      end
    end

    # error whose message can be shown to any user
    class UserError < Error
      cattr_accessor :user_error_classes
      self.user_error_classes = [self,
                                 ActionController::BadRequest,
                                 ActionController::MissingFile,
                                 ActiveRecord::RecordNotFound,
                                 ActiveRecord::RecordInvalid]
    end

    # error in CQL query
    class BadQuery < UserError
    end

    class BadAddress < UserError
      self.status_code = 404
      self.view = :bad_address
    end

    # card not found
    class NotFound < UserError
      self.status_code = 404
      self.view = :not_found
    end

    class CodenameNotFound < NotFound
    end

    # two editors altering the same card at once
    class EditConflict < UserError
      self.status_code = 409
      self.view = :conflict
    end

    # permission errors
    class PermissionDenied < UserError
      self.status_code = 403
      self.view = :denial

      def card_message_text
        card.errors[:permission_denied]
      end
    end

    # exception class for aborting card actions
    class Abort < StandardError
      attr_reader :status

      def report
        Rails.logger.debug "aborting: #{message}"
      end

      def initialize status, msg=""
        @status = status
        super msg
      end
    end

    # associating views with exceptions
    class << self
      KEY_MAP = { permission_denied: PermissionDenied,
                  conflict: EditConflict }.freeze

      def report exception, card
        e = cardify_exception exception, card
        self.current = e
        e.report
        e
      end

      def cardify_exception exception, card
        card_exception =
          if exception.is_a? Card::Error
            exception
          else
            card_error_class(exception, card).new exception.message
          end
        card_exception.card ||= card
        card_exception.backtrace ||= exception.backtrace
        add_card_errors card, card_exception if card.errors.empty?
        card_exception
      end

      def add_card_errors card, exception
        label = exception.class.to_s.split("::").last
        card.errors.add label, exception.message
      end

      def card_error_class exception, card
        # "simple" error messages are visible to end users and are generally not
        # treated as software bugs (though they may be "shark" bugs)
        case exception
        when ActiveRecord::RecordInvalid
          invalid_card_error_class card
        when ActiveRecord::RecordNotFound, ActionController::MissingFile
          Card::Error::NotFound
        else
          Card::Error::ServerError
        end
      end

      def invalid_card_error_class card
        KEY_MAP.each do |key, klass|
          return klass if card.errors.key? key
        end
        Card::Error
      end
    end
  end
end