Class: Stannum::Errors

Inherits:
Object
  • Object
show all
Includes:
Enumerable
Defined in:
lib/stannum/errors.rb

Overview

An errors object represents a collection of errors.

Most of the time, an end user will not be creating an Errors object directly. Instead, an errors object may be returned by a process that validates or coerces data to an expected form. For one such example, see the Stannum::Constraint and its subclasses.

Internally, an errors object is an Array of errors. Each error is represented by a Hash containing the keys :data, :message, :path and :type.

  • The :type of the error is a short, unique symbol or string that identifies the type of the error, such as ‘invalid’ or ‘not_found’. The type is frequently namespaced, e.g. ‘stannum.constraints.present’.

  • The :message of the error is a short string that provides a human-readable description of the error, such as ‘is invalid’ or ‘is not found’. The message may include format directives for error data (see below). If the :message key is missing or the value is nil, use a default error message or generate the message from the :type.

  • The :data of the error stores additional information about the error and the expected behavior. For example, an out of range error might have type: ‘out_of_range’ and data { min: 0, max: 10 }, indicating that the expected values were between 0 and 10. If the data key is missing or the value is empty, there is no additional information about the error.

  • The :path of the error reflects the steps to resolve the relevant property from the given data object. The path is an Array with keys of either Symbols/Strings (for object properties or Hash keys) or Integers (for Array indices). For example, given the hash { companies: [{ teams: [] }] } and an expecation that a company’s team must not be empty, the resulting error would have path: [:companies, 0, :teams]. if the path key is missing or the value is empty, the error refers to the root object.

Examples:

Creating An Errors Object

errors = Stannum::Errors.new

Adding Errors

errors.add(:not_numeric)

# Add an error with a custom message.
errors.add(:invalid, message: 'is not valid')

# Add an error with additional data.
errors.add(:out_of_range, min: 0, max: 10)

# Add multiple errors.
errors.add(:first_error).add(:second_error).add(:third_error)

Viewing The Errors

errors.empty? #=> false
errors.size   #=> 6

errors.each { |err| } #=> yields each error to the block
errors.to_a           #=> returns an array containing each error

Accessing Nested Errors via a Key

errors = Stannum::Errors.new
child  = errors[:spell]
child.size #=> 0
child.to_a #=> []

child.add(:insufficient_mana)
child.size # 1
child.to_a # [{ type: :insufficient_mana, path: [] }]

# Adding an error to a child makes it available on a parent.
errors.size # 1
errors.to_a # [{ type: :insufficient_mana, path: [:spell] }]

Accessing Nested Errors via an Index

errors = Stannum::Errors.new
child  = errors[1]

child.size #=> 0
child.to_a #=> []

child.add(:unknown_monster)
child.size # 1
child.to_a # [{ type: :unknown_monster, path: [] }]

# Adding an error to a child makes it available on a parent.
errors.size # 1
errors.to_a # [{ type: :unknown_monster, path: [1] }]

Accessing Deeply Nested Errors

errors = Stannum::Errors.new

errors[:towns][1][:name].add(:unpronounceable)
errors.size #=> 1
errors.to_a #=> [{ type: :unpronounceable, path: [:towns, 1, :name] }]

errors[:towns].size #=> 1
errors[:towns].to_a #=> [{ type: :unpronounceable, path: [1, :name] }]

errors[:towns][1].size #=> 1
errors[:towns][1].to_a #=> [{ type: :unpronounceable, path: [:name] }]

errors[:towns][1][:name].size #=> 1
errors[:towns][1][:name].to_a #=> [{ type: :unpronounceable, path: [] }]

# Can also access nested properties via #dig.
errors.dig(:towns, 1, :name).to_a #=> [{ type: :unpronounceable, path: [] }]

Replacing Errors

errors = Cuprum::Errors.new
errors[:potions][:ingredients].add(:missing_rabbits_foot)
errors.size #=> 1

other = Cuprum::Errors.new.add(:too_hot, :brew_longer, :foul_smelling)
errors[:potions] = other
errors.size #=> 3
errors.to_a
#=> [
#     { type: :brew_longer, path: [:potions] },
#     { type: :foul_smelling, path: [:potions] },
#     { type: :too_hot, path: [:potions] }
#   ]

Replacing Nested Errors

errors = Cuprum::Errors.new
errors[:armory].add(:empty)

other = Cuprum::Errors.new
other.dig(:weapons, 0).add(:needs_sharpening)
other.dig(:weapons, 1).add(:rusty).add(:out_of_ammo)

errors[:armory] = other
errors.size #=> 3
errors.to_a
#=> [
#     { type: needs_sharpening, path: [:armory, :weapons, 0] },
#     { type: out_of_ammo, path: [:armory, :weapons, 1] },
#     { type: rusty, path: [:armory, :weapons, 1] }
#   ]

Instance Method Summary collapse

Constructor Details

#initializeErrors

Returns a new instance of Errors.



143
144
145
146
147
# File 'lib/stannum/errors.rb', line 143

def initialize
  @children = Hash.new { |hsh, key| hsh[key] = self.class.new }
  @cache    = Set.new
  @errors   = []
end

Instance Method Details

#==(other) ⇒ true, false Also known as: eql?

Checks if the other errors object contains the same errors.

Returns:

  • (true, false)

    true if the other object is an errors object or an array with the same class and errors, otherwise false.



153
154
155
156
157
158
159
# File 'lib/stannum/errors.rb', line 153

def ==(other)
  return false unless other.is_a?(Array) || other.is_a?(self.class)

  return false unless empty? == other.empty?

  compare_hashed_errors(other)
end

#[](key) ⇒ Stannum::Errors

Accesses a nested errors object.

Each errors object can have one or more children, each of which is itself an errors object. These nested errors represent errors on some subset of the main object - for example, a failed validation of a named property, of the value in a key-value pair, or of an indexed value in an ordered collection.

The children are created as needed and are stored with either an integer or a symbol key. Calling errors multiple times will always return the same errors object. Likewise, calling errors multiple times will return the same object, and calling errors will return that same errors object as well.

Examples:

Accessing Nested Errors via a Key

errors = Stannum::Errors.new
child  = errors[:spell]
child.size #=> 0
child.to_a #=> []

child.add(:insufficient_mana)
child.size # 1
child.to_a # [{ type: :insufficient_mana, path: [] }]

# Adding an error to a child makes it available on a parent.
errors.size # 1
errors.to_a # [{ type: :insufficient_mana, path: [:spell] }]

Accessing Nested Errors via an Index

errors = Stannum::Errors.new
child  = errors[1]

child.size #=> 0
child.to_a #=> []

child.add(:unknown_monster)
child.size # 1
child.to_a # [{ type: :unknown_monster, path: [] }]

# Adding an error to a child makes it available on a parent.
errors.size # 1
errors.to_a # [{ type: :unknown_monster, path: [1] }]

Accessing Deeply Nested Errors

errors = Stannum::Errors.new

errors[:towns][1][:name].add(:unpronounceable)
errors.size #=> 1
errors.to_a #=> [{ type: :unpronounceable, path: [:towns, 1, :name] }]

errors[:towns].size #=> 1
errors[:towns].to_a #=> [{ type: :unpronounceable, path: [1, :name] }]

errors[:towns][1].size #=> 1
errors[:towns][1].to_a #=> [{ type: :unpronounceable, path: [:name] }]

errors[:towns][1][:name].size #=> 1
errors[:towns][1][:name].to_a #=> [{ type: :unpronounceable, path: [] }]

Parameters:

  • key (Integer, String, Symbol)

    The key or index of the referenced value, item, or property.

Returns:

Raises:

  • (ArgumentError)

    if the key is not a String, Symbol or Integer.

See Also:



231
232
233
234
235
# File 'lib/stannum/errors.rb', line 231

def [](key)
  validate_key(key)

  @children[key]
end

#[]=(key, value) ⇒ Object

Replaces the child errors with the specified errors object or Array.

If the given value is nil or an empty array, the #[]= operator will remove the child errors object at the given key, removing all errors within that namespace and all namespaces nested inside it.

If the given value is an errors object or an Array of errors object, the #[]= operation will replace the child errors object at the given key, removing all existing errors and adding the new errors. Each added error will use its nested path (if any) as a relative path from the given key.

Examples:

Replacing Errors

errors = Cuprum::Errors.new
errors[:potions][:ingredients].add(:missing_rabbits_foot)
errors.size #=> 1

other = Cuprum::Errors.new.add(:too_hot, :brew_longer, :foul_smelling)
errors[:potions] = other
errors.size #=> 3
errors.to_a
#=> [
#     { type: :brew_longer, path: [:potions] },
#     { type: :foul_smelling, path: [:potions] },
#     { type: :too_hot, path: [:potions] }
#   ]

Replacing Nested Errors

errors = Cuprum::Errors.new
errors[:armory].add(:empty)

other = Cuprum::Errors.new
other.dig(:weapons, 0).add(:needs_sharpening)
other.dig(:weapons, 1).add(:rusty).add(:out_of_ammo)

errors[:armory] = other
errors.size #=> 3
errors.to_a
#=> [
#     { type: needs_sharpening, path: [:armory, :weapons, 0] },
#     { type: out_of_ammo, path: [:armory, :weapons, 1] },
#     { type: rusty, path: [:armory, :weapons, 1] }
#   ]

Parameters:

  • key (Integer, String, Symbol)

    The key or index of the referenced value, item, or property.

  • value (Stannum::Errors, Array[Hash], nil)

    The errors to insert with the specified path.

Returns:

  • (Object)

    the value passed in.

Raises:

  • (ArgumentError)

    if the key is not a String, Symbol or Integer.

  • (ArgumentError)

    if the value is not a valid errors object, Array of errors hashes, empty Array, or nil.

See Also:



294
295
296
297
298
299
300
# File 'lib/stannum/errors.rb', line 294

def []=(key, value)
  validate_key(key)

  value = normalize_value(value, allow_nil: true)

  @children[key] = value
end

#add(type, message: nil, **data) ⇒ Stannum::Errors

Adds an error of the specified type.

Examples:

Adding An Error

errors = Stannum::Errors.new.add(:not_found)

Adding An Error With A Message

errors = Stannum::Errors.new.add(:not_found, message: 'is missing')

Adding Multiple Errors

errors = Stannum::Errors.new
errors
  .add(:not_numeric)
  .add(:not_integer, message: 'is outside the range')
  .add(:not_in_range)

Parameters:

  • type (String, Symbol)

    The error type. This should be a string or symbol with one or more underscored, dot-separated values.

  • message (String) (defaults to: nil)

    A custom error message to display. Optional; defaults to nil.

  • data (Hash<Symbol, Object>)

    Additional data to store about the error, such as the expected type or the min/max values of the expected range. Optional; defaults to an empty Hash.

Returns:

Raises:

  • (ArgumentError)

    if the type or message are invalid.



328
329
330
331
332
333
334
335
336
337
338
# File 'lib/stannum/errors.rb', line 328

def add(type, message: nil, **data)
  error  = build_error(data: data, message: message, type: type)
  hashed = error.hash

  return self if @cache.include?(hashed)

  @errors << error
  @cache  << hashed

  self
end

#dig(keys) ⇒ Stannum::Errors #dig(*keys) ⇒ Stannum::Errors

Accesses a (possibly deeply) nested errors object.

Similiar to the #[] method, but can access a deeply nested errors object as well. The #dig method can take either a list of one or more keys (Integers, Strings, and Symbols) as arguments, or an Array of keys. Calling errors.dig is equivalent to calling errors[] with each key in sequence.

Examples:

Accessing Nested Errors via a Key

errors = Stannum::Errors.new
child  = errors.dig(:spell)
child.size #=> 0
child.to_a #=> []

child.add(:insufficient_mana)
child.size # 1
child.to_a # [{ type: :insufficient_mana, path: [] }]

# Adding an error to a child makes it available on a parent.
errors.size # 1
errors.to_a # [{ type: :insufficient_mana, path: [:spell] }]

Accessing Nested Errors via an Index

errors = Stannum::Errors.new
child  = errors.dig(1)

child.size #=> 0
child.to_a #=> []

child.add(:unknown_monster)
child.size # 1
child.to_a # [{ type: :unknown_monster, path: [] }]

# Adding an error to a child makes it available on a parent.
errors.size # 1
errors.to_a # [{ type: :unknown_monster, path: [1] }]

Accessing Deeply Nested Errors

errors = Stannum::Errors.new

errors.dig(:towns, 1, :name).add(:unpronounceable)
errors.size #=> 1
errors.to_a #=> [{ type: :unpronounceable, path: [:towns, 1, :name] }]

errors.dig(:towns).size #=> 1
errors.dig(:towns).to_a #=> [{ type: :unpronounceable, path: [1, :name] }]

errors.dig(:towns, 1).size #=> 1
errors.dig(:towns, 1).to_a #=> [{ type: :unpronounceable, path: [:name] }]

errors.dig(:towns, 1, :name).size #=> 1
errors.dig(:towns, 1, :name).to_a #=> [{ type: :unpronounceable, path: [] }]

Overloads:

  • #dig(keys) ⇒ Stannum::Errors

    Parameters:

    • keys (Array<Integer, String, Symbol>)

      The path to the nested errors object, as an array of Integers, Strings, and Symbols.

  • #dig(*keys) ⇒ Stannum::Errors

    Parameters:

    • keys (Array<Integer, String, Symbol>)

      The path to the nested errors object, as individual Integers, Strings, and Symbols.

Returns:

Raises:

  • (ArgumentError)

    if the keys are not Strings, Symbols or Integers.

See Also:



406
407
408
409
410
# File 'lib/stannum/errors.rb', line 406

def dig(first, *rest)
  path = first.is_a?(Array) ? first : [first, *rest]

  path.reduce(self) { |errors, segment| errors[segment] }
end

#dupStannum::Errors

Creates a deep copy of the errors object.

Returns:



415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
# File 'lib/stannum/errors.rb', line 415

def dup # rubocop:disable Metrics/MethodLength
  child = self.class.new

  each do |error|
    child # rubocop:disable Style/SingleArgumentDig
      .dig(error.fetch(:path, []))
      .add(
        error.fetch(:type),
        message: error[:message],
        **error.fetch(:data, {})
      )
  end

  child
end

#eachEnumerator #each {|error| ... } ⇒ Object

Overloads:

  • #eachEnumerator

    Returns an Enumerator that iterates through the errors.

    Returns:

    • (Enumerator)
  • #each {|error| ... } ⇒ Object

    Iterates through the errors, yielding each error to the provided block.

    Yield Parameters:

    • error (Hash<Symbol=>Object>)

      The error object. Each error is a hash containing the keys :data, :message, :path and :type.



441
442
443
444
445
446
447
448
449
450
451
# File 'lib/stannum/errors.rb', line 441

def each
  return to_enum(:each) { size } unless block_given?

  @errors.each { |item| yield item.merge(path: []) }

  @children.each do |path, child|
    child.each do |item|
      yield item.merge(path: item.fetch(:path, []).dup.unshift(path))
    end
  end
end

#empty?true, false Also known as: blank?

Checks if the errors object contains any errors.

Returns:

  • (true, false)

    true if the errors object has no errors, otherwise false.



457
458
459
# File 'lib/stannum/errors.rb', line 457

def empty?
  @errors.empty? && @children.all?(&:empty?)
end

#group_by_pathHash<Array, Array> #group_by_path {|error| ... } ⇒ Hash<Array, Array>

Groups the errors by the error path.

Generates a Hash whose keys are the unique error :path values. For each path, the corresponding value is the Array of all errors with that path.

This will flatten paths: an error with path [:parts] will be grouped in a separate array from a part with path [:parts, :assemblies].

Errors with an empty path will be grouped with a key of an empty Array.

Overloads:

  • #group_by_path {|error| ... } ⇒ Hash<Array, Array>

    Groups the values returned by the block by the error path.

    Yield Parameters:

    • error (Hash<Symbol>)

      the error Hash.

Returns:

  • (Hash<Array, Array>)

    the errors grouped by the error path.



480
481
482
483
484
485
486
487
488
489
490
491
# File 'lib/stannum/errors.rb', line 480

def group_by_path
  grouped = Hash.new { |hsh, key| hsh[key] = [] }

  each do |error|
    path  = error[:path]
    value = block_given? ? yield(error) : error

    grouped[path] << value
  end

  grouped
end

#inspectString

Returns a human-readable representation of the object.

Returns:

  • (String)

    a human-readable representation of the object.



494
495
496
497
498
# File 'lib/stannum/errors.rb', line 494

def inspect
  oid = super[2...].split.first.split(':').last

  "#<#{self.class.name}:#{oid} @summary=%{#{summary}}>"
end

#merge(value) ⇒ Stannum::Errors

Adds the given errors to a copy of the errors object.

Creates a copy of the errors object, and then adds each error in the passed in errors object or array to the copy. The copy will thus contain all of the errors from the original object and all of the errors from the passed in object. The original object is not changed.

Parameters:

  • value (Stannum::Errors, Array[Hash])

    The errors to add to the copied errors object.

Returns:

Raises:

  • (ArgumentError)

    if the value is not a valid errors object or Array of errors hashes.

See Also:



516
517
518
519
520
# File 'lib/stannum/errors.rb', line 516

def merge(value)
  value = normalize_value(value, allow_nil: false)

  dup.update_errors(value)
end

#sizeInteger Also known as: count

The number of errors in the errors object.

Returns:

  • (Integer)

    the number of errors.



525
526
527
528
529
# File 'lib/stannum/errors.rb', line 525

def size
  @errors.size + @children.each_value.reduce(0) do |total, child|
    total + child.size
  end
end

#summaryString

Generates a text summary of the errors.

Returns:

  • (String)

    the text summary.



535
536
537
538
539
# File 'lib/stannum/errors.rb', line 535

def summary
  with_messages
    .map { |error| generate_summary_item(error) }
    .join(', ')
end

#to_aArray<Hash>

Generates an array of error objects.

Each error is a hash containing the keys :data, :message, :path and :type.

Returns:

  • (Array<Hash>)

    the error objects.



546
547
548
# File 'lib/stannum/errors.rb', line 546

def to_a
  each.to_a
end

#update(value) ⇒ self

Adds the given errors to the errors object.

Adds each error in the passed in errors object or array to the current errors object. It will then contain all of the original errors and all of the errors from the passed in object. This changes the current object.

Parameters:

  • value (Stannum::Errors, Array[Hash])

    The errors to add to the current errors object.

Returns:

  • (self)

    the current errors object.

Raises:

  • (ArgumentError)

    if the value is not a valid errors object or Array of errors hashes.

See Also:



565
566
567
568
569
# File 'lib/stannum/errors.rb', line 565

def update(value)
  value = normalize_value(value, allow_nil: false)

  update_errors(value)
end

#with_messages(force: false, strategy: nil) ⇒ Stannum::Errors

Creates a copy of the errors and generates error messages for each error.

Parameters:

  • force (Boolean) (defaults to: false)

    If true, overrides any messages already defined for the errors.

  • strategy (#call) (defaults to: nil)

    The strategy to use to generate the error messages.

Returns:



579
580
581
582
583
584
585
586
587
588
589
590
591
# File 'lib/stannum/errors.rb', line 579

def with_messages(force: false, strategy: nil)
  strategy ||= Stannum::Messages.strategy

  dup.tap do |errors|
    errors.each_error do |error|
      next unless force || error[:message].nil? || error[:message].empty?

      message = strategy.call(error[:type], **(error[:data] || {}))

      error[:message] = message
    end
  end
end