diff options
| author | Murilo Ijanc <[email protected]> | 2026-07-26 14:40:05 -0300 |
|---|---|---|
| committer | Murilo Ijanc <[email protected]> | 2026-07-26 14:40:05 -0300 |
| commit | 0bc578d6d334c7f1ccaf329cd61bb8bce76b7f59 (patch) | |
| tree | 86f73108f0432fd8330edcaab8b8ae7018322dee /lib/Cepis/Error.pm | |
| download | perl-0bc578d6d334c7f1ccaf329cd61bb8bce76b7f59.tar.gz | |
Initial importv0.1.0
Diffstat (limited to 'lib/Cepis/Error.pm')
| -rw-r--r-- | lib/Cepis/Error.pm | 226 |
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 |