# frozen_string_literal: true require 'luna_park/tools' require 'luna_park/extensions/exceptions/substitutive' require 'i18n' if LunaPark::Tools.gem_installed?('i18n') module LunaPark module Errors # This class extends standard exception with a few things: # - define custom message with internalization key, if necessary # - setup handler behavior - raise or catch # - determine whether this error should be notified # - and if it should, define severity level # - send to handler not only message but also details # # @example Fatalism class # module Errors # class Fatalism < LunaPark::Errors::Base # message 'You cannot change your destiny', i18n: 'errors.fatalism' # notify: :info # end # end # # error = Error::Fatalism.new(choose: 'The first one') # error.message # => 'You cannot change your destiny' # error.message(lang: :ru) # => 'Вы не можете выбрать свою судьбу' # error.details # => { :choose => "The first one" } # class Base < StandardError extend Extensions::Exceptions::Substitutive NOTIFY_VALUES = [true, false, :debug, :info, :warning, :error, :fatal, :unknown].freeze NOTIFY_LEVELS = %i[debug info warning error fatal unknown].freeze DEFAULT_NOTIFY_LEVEL = :error private_constant :NOTIFY_VALUES, :NOTIFY_LEVELS, :DEFAULT_NOTIFY_LEVEL class << self # Explains how this error class will be notified # # @return [Boolean, Symbol] the behavior of the notification attr_reader :default_notify # What the key of the translation was selected for this error # # @return [NilClass, String] internationalization key attr_reader :i18n_key # Proc, that receives details hash: { detail_key => detail_value } # # @private attr_reader :__default_message_block__ # Specifies the expected behavior of the error handler if an error # instance of this class is raised # # @param [Symbol] - set behavior of the notification (see #default_notify) # # @return [NilClass] def notify(lvl) self.default_notify = lvl unless lvl.nil? nil end # Specify default error message # # @param txt [String] - text of message # @param i18n [String] - internationalization key # @return [NilClass] def message(txt = nil, i18n_key: nil, i18n: nil, &default_message_block) @__default_message_block__ = block_given? ? default_message_block : txt && ->(_) { txt } @i18n_key = i18n || i18n_key nil end def inherited(inheritor) if __default_message_block__ inheritor.message(i18n_key: i18n_key, &__default_message_block__) elsif i18n_key inheritor.message(i18n_key: i18n_key) end inheritor.default_notify = default_notify super end protected def default_notify=(notify) raise ArgumentError, "Unexpected notify value #{notify}" unless NOTIFY_VALUES.include? notify @default_notify = notify end end notify false # It is additional information which extends the notification message # # @example # error = Fatalism.new('Message text', custom: 'Some important', foo: Foo.new ) # error.details # => {:custom=>"Some important", :foo=>#<Foo:0x000055b70ef6c370>} attr_reader :details # Create new error # # @param msg - Message text # @param notify - defines notifier behaviour (see #self.notify) # @param details - additional information to notifier # # @example without parameters # error = Fatalism.new # error.message # => 'You cannot change your destiny' # error.notify_lvl # => :error # error.notify? # => true # # @example with custom parameters # @error = Fatalism.new 'Forgive me Kuzma, my feet are frozen', notify: false # error.message # => 'Forgive Kuzma, my feet froze' # error.notify_lvl # => :error # error.notify? # => false # # TODO: make guards safe: remove these raises from exception constructor (from runtime) def initialize(msg = nil, notify: nil, **details) raise ArgumentError, "Unexpected notify value: #{notify}" unless notify.nil? || NOTIFY_VALUES.include?(notify) @message = msg @notify = notify @details = details super(message) end # Should the handler send this notification ? # # @return [Boolean] it should be notified? # # @example notify is undefined # error = LunaPark::Errors::Base # error.notify # => false def notify? @notify || self.class.default_notify ? true : false end # Severity level for notificator # # @return [Symbol] expected notification level # # @example notify is undefined # error = LunaPark::Errors::Base # error.notify_lvl # => :error # def notify_lvl return @notify if NOTIFY_LEVELS.include? @notify return self.class.default_notify if NOTIFY_LEVELS.include? self.class.default_notify DEFAULT_NOTIFY_LEVEL end # Error message # # The message text is defined in the following order: # 1. In the `initialize` method # 2. Translated message, if i18n key was settled in class (see `.message`) # 3. In the class method (see `.message`) # # @param locale [Symbol,String] # @return [String] message text # # @example message is not settled # LunaPark::Errors::Base.new.message # => 'LunaPark::Errors::Base' # # @example message is defined in class # class WrongAnswerError < LunaPark::Errors::Base # message 'Answer is 42' # end # # WrongAnswerError.new.message # => 'Answer is 42' # # @example message is in internatialization config # # I18n YML # # ru: # # errors: # # frost: Прости Кузьма, замерзли ноги! # # class FrostError < LunaPark::Errors::Base # message 'Forgive Kuzma, my feet froze', i18n: 'errors.frost' # end # # error = FrostError.new # error.message(locale: :ru) # => 'Прости Кузьма, замерзли ноги!' # # @example message is defined in class with block # class WrongAnswerError < LunaPark::Errors::Base # message { |details| "Answer is '#{details[:correct]}' - not '#{details[:wrong]}'" } # end # # error = WrongAnswerError.new(correct: 42, wrong: 420) # error.message # => "Answer is '42' - not '420'" # # @example message is in internalization config with i18n interpolation # # I18n YML # # de: # # errors: # # wrong_answer: Die richtige Antwort ist '%{correct}', nicht '%{wrong}' # # class WrongAnswerError < LunaPark::Errors::Base # message i18n: 'errors.wrong_answer' # end # # error = WrongAnswerError.new(correct: 42, wrong: 420) # error.message(locale: :de) # => "Die richtige Antwort ist '42', nicht '420'" # def message(locale: nil) return @message if @message default_message = build_default_message localized_message(locale, show_error: default_message.nil?) || default_message || self.class.name end private # Return translation of an error message if 18n_key is defined # if `show_error: true` and translation is missing, will return string 'translation missing: path' # if `show_error: false` and translation is missing, will return nil # # @param locale [Symbol] - specified locale # @return [String] - Translated text def localized_message(locale = nil, show_error:) return unless self.class.i18n_key return unless show_error || I18n.exists?(self.class.i18n_key) I18n.t(self.class.i18n_key, locale: locale, **details) end # @return [String] - Default message def build_default_message self.class.__default_message_block__&.call(details) end end end end