summaryrefslogtreecommitdiffstats
path: root/lib/Cepis/Error.pm
diff options
context:
space:
mode:
authorMurilo Ijanc <[email protected]>2026-07-26 14:40:05 -0300
committerMurilo Ijanc <[email protected]>2026-07-26 14:40:05 -0300
commit0bc578d6d334c7f1ccaf329cd61bb8bce76b7f59 (patch)
tree86f73108f0432fd8330edcaab8b8ae7018322dee /lib/Cepis/Error.pm
downloadperl-6661cb35162c38099b86f248b938f5e4d4d11515.tar.gz
Initial importv0.1.0
Diffstat (limited to 'lib/Cepis/Error.pm')
-rw-r--r--lib/Cepis/Error.pm226
1 files changed, 226 insertions, 0 deletions
diff --git a/lib/Cepis/Error.pm b/lib/Cepis/Error.pm
new file mode 100644
index 0000000..080b0a1
--- /dev/null
+++ b/lib/Cepis/Error.pm
@@ -0,0 +1,226 @@
+package Cepis::Error;
+
+use strict;
+use warnings;
+
+our $VERSION = '0.1.0';
+
+use overload
+ '""' => sub { $_[0]->message },
+ 'bool' => sub { 1 },
+ fallback => 1;
+
+sub new {
+ my ( $class, %args ) = @_;
+ my $self = {%args};
+ $self->{message} = $class unless defined $self->{message};
+ return bless $self, $class;
+}
+
+sub throw {
+ my $class = shift;
+ die $class->new(@_);
+}
+
+sub message { return $_[0]->{message} }
+
+package Cepis::Error::InvalidCEP;
+
+use strict;
+use warnings;
+use parent -norequire, 'Cepis::Error';
+
+our $VERSION = '0.1.0';
+
+package Cepis::Error::CEPNotFound;
+
+use strict;
+use warnings;
+use parent -norequire, 'Cepis::Error';
+
+our $VERSION = '0.1.0';
+
+package Cepis::Error::Network;
+
+use strict;
+use warnings;
+use parent -norequire, 'Cepis::Error';
+
+our $VERSION = '0.1.0';
+
+package Cepis::Error::ServerError;
+
+use strict;
+use warnings;
+use parent -norequire, 'Cepis::Error';
+
+our $VERSION = '0.1.0';
+
+sub status { return $_[0]->{status} }
+
+package Cepis::Error::RateLimited;
+
+use strict;
+use warnings;
+use parent -norequire, 'Cepis::Error';
+
+our $VERSION = '0.1.0';
+
+sub retry_after { return $_[0]->{retry_after} }
+
+package Cepis::Error::InvalidResponse;
+
+use strict;
+use warnings;
+use parent -norequire, 'Cepis::Error';
+
+our $VERSION = '0.1.0';
+
+1;
+
+__END__
+
+=encoding UTF-8
+
+=head1 NAME
+
+Cepis::Error - exception hierarchy for L<Cepis>
+
+=head1 SYNOPSIS
+
+ use Cepis;
+
+ my $address = eval { Cepis::lookup($cep_from_form) };
+ if ( !$address ) {
+ my $err = $@;
+ if ( ref $err && $err->isa('Cepis::Error::CEPNotFound') ) {
+ # the CEP does not exist: ask the user for another
+ }
+ elsif ( ref $err && $err->isa('Cepis::Error') ) {
+ # network, server, rate limit: retry later
+ }
+ else {
+ die $err;
+ }
+ }
+
+=head1 DESCRIPTION
+
+Every failure raised by this library is an object thrown with C<die>, and every
+one of them inherits from C<Cepis::Error>. One C<< ref $@ && $@->isa('Cepis::Error') >>
+therefore catches the lot.
+
+Below the base class the split is by B<the decision the caller has to make>,
+not by the HTTP status that caused it:
+
+=over 4
+
+=item *
+
+L</Cepis::Error::InvalidCEP> and L</Cepis::Error::CEPNotFound> are problems with
+the I<request>. Retrying is pointless — the same CEP will fail again.
+
+=item *
+
+L</Cepis::Error::Network>, L</Cepis::Error::ServerError> and
+L</Cepis::Error::RateLimited> are problems with I<transport>. Retrying may help,
+and the client already retried on its own before throwing any of them.
+
+=back
+
+That split is why C<CEPNotFound> is not "the HTTP 404 exception": C<InvalidCEP>
+is thrown without any request being made at all, when the CEP is malformed
+before it ever leaves the machine.
+
+Every error stringifies to its message, so C<warn $@> and C<"$@"> do the
+obvious thing.
+
+=head1 CLASSES
+
+=head2 Cepis::Error
+
+Base of every error. The only accessor it guarantees is C<message>.
+
+=over 4
+
+=item C<< Cepis::Error->new( message => $text, %fields ) >>
+
+Builds the object. Extra fields are stored as-is and become available to
+subclasses.
+
+=item C<< Cepis::Error->throw( message => $text, %fields ) >>
+
+Builds the object and C<die>s with it.
+
+=item C<< $err->message >>
+
+The human-readable message. Also what the object stringifies to.
+
+=back
+
+=head2 Cepis::Error::InvalidCEP
+
+The CEP is not 8 digits.
+
+Usually thrown B<without> a request ever being made — the client checks the
+format before going to the network. It is also thrown when the API answers 400
+to a CEP that passed local validation, in which case it carries the API's own
+message.
+
+=head2 Cepis::Error::CEPNotFound
+
+The CEP is well-formed but does not exist in the DNE database.
+
+=head2 Cepis::Error::Network
+
+The request never completed: DNS, refused connection, timeout, TLS.
+
+Thrown only after every configured attempt has failed — with one deliberate
+exception. When C<base_url> is C<https:> and Perl has no TLS support installed,
+this is thrown immediately, before any attempt, with a message naming the two
+modules to install. Retrying cannot fix a missing module. See
+L<Cepis::Client/"HTTPS support">.
+
+=head2 Cepis::Error::ServerError
+
+The API answered 5xx on every attempt.
+
+=over 4
+
+=item C<< $err->status >>
+
+The HTTP status seen on the last attempt.
+
+=back
+
+=head2 Cepis::Error::RateLimited
+
+The API, or the edge in front of it, answered 429 on every attempt.
+
+=over 4
+
+=item C<< $err->retry_after >>
+
+What the server asked for, in seconds, or C<undef> if it did not say. The
+client already honoured this across the attempts it made; it is exposed for
+callers who want to reschedule the work.
+
+=back
+
+=head2 Cepis::Error::InvalidResponse
+
+A success status came back with a body that cannot be parsed.
+
+In practice this means something between you and the API is answering instead
+of it — a captive Wi-Fi portal, a corporate proxy, an HTML error page. Useful
+as a diagnosis: the problem is not in your code.
+
+=head1 SEE ALSO
+
+L<Cepis>, L<Cepis::Client>
+
+=head1 LICENCE
+
+ISC. Copyright (c) 2026 Murilo Ijanc E<lt>[email protected]<gt>.
+
+=cut