Exception: Magik::Error

Inherits:
StandardError
  • Object
show all
Defined in:
lib/magik.rb

Overview

Base class for every error Magik raises.

Magik errors are not free-form strings. Each one carries three things, and all three are required:

  1. a stable code matching CODE_FORMAT, shaped MAGIK_<SUBSYSTEM>_<CONDITION>, that is safe to grep for, link to, and match on in tests and CI;
  2. a cause — one sentence saying what actually went wrong;
  3. a fix — a runnable command or a concrete edit, never "check your configuration".

The rendered message is deterministic:

MAGIK_LEDGER_UNBALANCED: entries for :Payouts do not balance (debits 1200, credits 900).
  fix: run `magik check --ledger Payouts`

Subclasses declare their defaults with the Error.code and Error.fix class-level DSL, so a raise site only has to supply the cause.

Examples:

Raise a one-off error

raise Magik::Error.new(
  "the app has no `App.define` block",
  code: "MAGIK_BOOT_NO_APP",
  fix: "run `magik new myapp` to generate one"
)

Declare a reusable error class

class NoTenant < Magik::Error
  code "MAGIK_MODEL_NO_TENANT"
  fix  "magik generate model Invoice --tenant"
end
raise NoTenant, "model :Invoice declares no tenant_id and no `global!` reason"

Constant Summary collapse

CODE_FORMAT =

The shape every Magik error code must take: MAGIK_ followed by underscore-separated uppercase words.

Returns:

  • (Regexp)
/\AMAGIK_[A-Z0-9]+(?:_[A-Z0-9]+)*\z/
DEFAULT_CODE =

Fallback code for Magik::Error itself.

Returns:

  • (String)
"MAGIK_ERROR"
DEFAULT_FIX =

Fallback fix for Magik::Error itself.

Returns:

  • (String)
"read the raised cause above, then see docs/idea/00-build-spec.md"

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(cause_text = nil, code: self.class.code, fix: self.class.fix) ⇒ Error

Returns a new instance of Error.

Parameters:

  • cause_text (String) (defaults to: nil) —

    what went wrong, one sentence

  • code (String) (defaults to: self.class.code) —

    a code matching CODE_FORMAT; defaults to the class-level code

  • fix (String) (defaults to: self.class.fix) —

    an actionable fix; defaults to the class-level fix

Raises:

  • (ArgumentError) —

    if the code is malformed or the fix is blank



207
208
209
210
211
212
# File 'lib/magik.rb', line 207

def initialize(cause_text = nil, code: self.class.code, fix: self.class.fix)
  @code = Error.validate_code!(code)
  @cause_text = Error.normalize_cause(cause_text, self.class)
  @fix = Error.validate_fix!(fix)
  super(self.class.render(@code, @cause_text, @fix))
end

Instance Attribute Details

#cause_text ⇒ String (readonly)

What went wrong, in one sentence.

Named cause_text because Exception#cause is reserved by Ruby for the exception that was in flight when this one was raised.

Returns:

  • (String)


195
196
197
# File 'lib/magik.rb', line 195

def cause_text
  @cause_text
end

#code ⇒ String (readonly)

The stable, greppable error code.

Returns:

  • (String) —

    e.g. "MAGIK_CLI_UNKNOWN_COMMAND"



187
188
189
# File 'lib/magik.rb', line 187

def code
  @code
end

#fix ⇒ String (readonly)

How to make it stop — a runnable command or a concrete edit.

Returns:

  • (String)


199
200
201
# File 'lib/magik.rb', line 199

def fix
  @fix
end

Class Method Details

.code(value = nil) ⇒ String

Get or set the default error code for this class and its subclasses.

Called with an argument it is a writer; called without, a reader that walks up the superclass chain.

Parameters:

  • value (String, nil) (defaults to: nil) —

    a code matching CODE_FORMAT, or nil to read

Returns:

  • (String) —

    the effective code for this class

Raises:

  • (ArgumentError) —

    if value does not match CODE_FORMAT



136
137
138
139
140
# File 'lib/magik.rb', line 136

def code(value = nil)
  return @code = Error.validate_code!(value) unless value.nil?

  @code || (superclass.respond_to?(:code) ? superclass.code : DEFAULT_CODE)
end

.fix(value = nil) ⇒ String

Get or set the default fix line for this class and its subclasses.

Parameters:

  • value (String, nil) (defaults to: nil) —

    an actionable fix, or nil to read

Returns:

  • (String) —

    the effective fix for this class

Raises:

  • (ArgumentError) —

    if value is blank



147
148
149
150
151
# File 'lib/magik.rb', line 147

def fix(value = nil)
  return @fix = Error.validate_fix!(value) unless value.nil?

  @fix || (superclass.respond_to?(:fix) ? superclass.fix : DEFAULT_FIX)
end

.normalize_cause(value, klass) ⇒ String

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns a frozen, non-empty cause.

Parameters:

  • value (String, nil) —

    candidate cause text

  • klass (Class) —

    the error class, used as the fallback cause

Returns:

  • (String) —

    a frozen, non-empty cause



168
169
170
171
# File 'lib/magik.rb', line 168

def normalize_cause(value, klass)
  string = value.to_s
  (string.strip.empty? ? klass.name.to_s : string).freeze
end

.render(code, cause_text, fix) ⇒ String

Render the canonical two-line message for an error triple.

Parameters:

  • code (String) —

    the error code

  • cause_text (String) —

    what went wrong

  • fix (String) —

    how to fix it

Returns:

  • (String) —

    "CODE: cause\n fix: fix"



220
221
222
# File 'lib/magik.rb', line 220

def self.render(code, cause_text, fix)
  "#{code}: #{cause_text}\n  fix: #{fix}"
end

.validate_code!(value) ⇒ String

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns the frozen, validated code.

Parameters:

  • value (String) —

    candidate error code

Returns:

  • (String) —

    the frozen, validated code

Raises:

  • (ArgumentError) —

    if the code is malformed



157
158
159
160
161
162
# File 'lib/magik.rb', line 157

def validate_code!(value)
  string = value.to_s
  return string.freeze if CODE_FORMAT.match?(string)

  raise ArgumentError, "error code #{string.inspect} must match #{CODE_FORMAT.source}"
end

.validate_fix!(value) ⇒ String

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns the frozen, validated fix.

Parameters:

  • value (String) —

    candidate fix line

Returns:

  • (String) —

    the frozen, validated fix

Raises:

  • (ArgumentError) —

    if the fix is blank



177
178
179
180
181
182
# File 'lib/magik.rb', line 177

def validate_fix!(value)
  string = value.to_s
  return string.freeze unless string.strip.empty?

  raise ArgumentError, "a Magik error needs a non-empty fix: line"
end

Instance Method Details

#to_h ⇒ Hash{Symbol => String}

The error as plain data, ready for --json output or a log line.

Returns:

  • (Hash{Symbol => String}) —

    with keys :code, :cause, :fix



227
228
229
# File 'lib/magik.rb', line 227

def to_h
  { code: code, cause: cause_text, fix: fix }
end