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 (MAGIK_SOMETHING) 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_NO_APP",
  fix: "run `magik new myapp` to generate one"
)

Declare a reusable error class

class MissingTenant < Magik::Error
  code "MAGIK_MISSING_TENANT"
  fix  "add `tenant_by :subdomain` to your App.define block"
end
raise MissingTenant, "query on :Invoice has no tenant_id in WHERE"

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



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

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_textString (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)


185
186
187
# File 'lib/magik.rb', line 185

def cause_text
  @cause_text
end

#codeString (readonly)

The stable, greppable error code.

Returns:

  • (String)

    e.g. "MAGIK_UNKNOWN_COMMAND"



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

def code
  @code
end

#fixString (readonly)

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

Returns:

  • (String)


189
190
191
# File 'lib/magik.rb', line 189

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



126
127
128
129
130
# File 'lib/magik.rb', line 126

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



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

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



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

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"



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

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



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

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



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

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_hHash{Symbol => String}

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

Returns:

  • (Hash{Symbol => String})

    with keys :code, :cause, :fix



217
218
219
# File 'lib/magik.rb', line 217

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