The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.
package Froody::Error;
use base qw(Exporter Error);
our @EXPORT_OK = qw(err);

use strict;
use warnings;

use Scalar::Util qw(blessed);
use Storable;
use YAML::Syck;

=head1 NAME

Froody::Error - Froody error class

=head1 SYNOPSIS

  use Froody::Error qw(err);
  
  # throw an error
  eval { 
    Froody::Error->throw("user.problem", "user '$userid' not known", $data);
  };

  # "user.problem, user '9BC6DD8C-1E25-11DA-98F1-DDB51046DF9C' not known, "
  print $@->code .", ". $@->msg

  # and a stacktrace
  print $@->stacktrace

  # print the data section
  print Dumper $@->data;
  
  # check if the error is the right class
  if (err("user"))
  {
    ...
  }

=head1 DESCRIPTION

Froody::Error is the Froody error class.  It's designed to be powerful, yet
simple to use.  It's a subclass of Error.pm.

Often, the easiest way to create an error response from within Froody is
to throw a Froody::Error.  This will be caught by the server, and it'll
be turned into an error response before dispatching it to the client.

Standard Froody::Error errors that are thrown by Froody itself indicating
that something is wrong with Froody (e.g. you've asked for a method that
doesn't exist, or you've ommited a required parameter) are listed in
L<Froody::Error::Standard>.

=head2 throw

To throw an error with Froody::Error you just have to call the throw method
with up to three arguments:

  # throw an error with its code, reporting the default message
  Froody::Error->throw("what");

  # throw an error with its code, reporting a custom message
  Froody::Error->throw("what", "bang");
  
  # throw an error with code, custome message, and some data
  Froody::Error->throw("what", bang", { thingy => "broken" });

In each case this creates a new Froody::Error object and then C<die>s with it.

The first argument is the C<code> that defines the I<type> of error that we're
throwing.  The second argument is a C<message>, a string which describes the
error. If this error is translated to a Froody error response then these will
be mapped to the C<code> and C<message> attributes of the C<< <err> >>
repectivly.  The third argument is the data, a set of parameters that decribe
in a computer understandable format what causes the error.  This data
potentially will be transformed into the children of the C<< <err> >> tag based
on what specification is in the C<errortypes> of the repository.

=head2 Hierarchal Error Messages

Error messages use 'dot' notation to indicate what error messages are
sub-types of other error messages.

For example:

  Froody::Error->throw("io", "Bad IO");

And, a more particular error:

  Froody::Error->throw("io.file", "There was a problem with the file");

And an even more particular error

  Froody::Error->throw("io.file.notfound", "File not found");

But all these methods can be detected with isa_err

  if ($@->isa_err("io")) { ... }

Or even better with the err functions (as this won't go "bang" if $@ is a
hashref)

  use Froody::Error qw(err);
  if (err("io")) { ... }

=cut

# err is documented
sub err
{ 
  no warnings 'uninitialized';
  
  my $code = shift;

  return unless length $@;       # no err? Not an error
  return 1 unless length $code;  # empty spec? Always true
  
  # Froody::Error?  Of the right code?
  if (blessed($@) && $@->isa("Froody::Error"))
    { return $@->isa_err($code) }
  
  # It's an unknown error, were we looking for that?
  # There's no easy way I can see to make this functionality overridable
  # after all, we're in a *function* here, not a method.
  return $code eq "unknown" || $code eq "999";
}

# isa_err is documented
sub isa_err 
{
  no warnings 'uninitialized';

  my $self = shift;
  my $err = shift;

  # empty string === root node === always match
  return 1 unless length $err;

  # if we've no code then we must fail, as we can't possibly match it
  return 0 unless length $self->code;
  
  # do we match? 
  $err = quotemeta($err);
  return $self->code =~ /^$err(?:\.|$)/
}

=head1 METHODS

=over

=item new( message )

define

=cut

sub new {
  my $self  = shift;
  local $Error::Debug = 1;
  local $Error::Depth = $Error::Depth + 1;
  my $code    = shift;
  my $text    = shift;
  my $data    = shift;
  $self->SUPER::new(
    -text => $text,
    -code => $code,
    -data => $data,
  );
}

=item code

Return the error code.  Read only.

=cut

sub code {
  no warnings 'uninitialized';
  length($_[0]->{-code}) ? $_[0]->{-code} : "unknown";
}

=item message / msg / text

Returns the error message.  Read only.

=cut

sub text    { shift->{-text} }
sub msg     { shift->{-text} }
sub message { shift->{-text} }

=item data

Return (a copy of) the error data.  Read only.

=cut

sub _dclone {
 return $_[0] unless ref $_[0];
 return Storable::dclone $_[0];
}

sub data { _dclone(shift->{-data}) }

=item stringify

Returns a string containing both the error code and the error text, and
a stacktrace.  This is what is returned if you try and use a Froody::Error
in string context.

=cut

sub stringify {
  no warnings 'uninitialized';
  my $self    = shift;
  
  my $strace = $self->stacktrace;
  $strace =~ s/^/  /mg;
  
  my $return = $self->code;
  $return .= " - " . $self->message if length($self->message);
  # don't call the ->data method here, as we don't really need to clone the data
  $return .= "\nData:\n".Dump($self->{-data}) if defined $self->{-data};
  return $return . "\nStack trace:\n$strace"
}

=back

=head1 BACKWARDS COMPATIBILITY

We used to have a different sort of error.  Rather than having hierarchal
error codes that looked like this:

  <err code="file.rwerr" message="unable to write to disk" />

We used to use simple numbers and have errors that looked like this:

  <err code="12" message="unable to write to disk" />

This is fine.  The key here to remember is that numbered error codes are a
subset of the hierarchal error codes.  The numbers are all just top level
errors that are made up of alphanumerics that are just digits.  It's just
a not very hierarchal hierarchal error code.

=head1 BUGS

None known.

Please report any bugs you find via the CPAN RT system.
L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Froody>

=head1 AUTHOR

Copyright Fotango 2005.  All rights reserved.

Please see the main L<Froody> documentation for details of who has worked
on this project.

This module is free software; you can redistribute it and/or modify it under
the same terms as Perl itself.

=head1 SEE ALSO

L<Froody::Error::Standard>, L<Froody::Response::Error>

=cut

1;