summaryrefslogtreecommitdiffstats
path: root/lib/Cepis/Faixa.pm
diff options
context:
space:
mode:
authorMurilo Ijanc <[email protected]>2026-07-27 20:34:36 -0300
committerMurilo Ijanc <[email protected]>2026-07-27 20:34:36 -0300
commitf2f3e5b5b22f5db49670391fb747a52fd492b9db (patch)
tree3d02d13f3111bdd7381470d3fd3db1f58e00bc8c /lib/Cepis/Faixa.pm
parent0bc578d6d334c7f1ccaf329cd61bb8bce76b7f59 (diff)
downloadperl-e755f72a4e6da7bc25f6ed5eb9d6578772759fde.tar.gz
Carry the municipality on a CEP that does not existHEADv0.2.0main
A CEP that fails is rarely a random number. It is usually someone who knows the city and guessed the digits, and the classic guess is the -000 that 957 municipalities never received. The API now answers those 404s with a faixa block naming the municipality that owns the band, so the library can say "25845000 does not exist in Areal/RJ" instead of "not found". It stays an error: lookup still throws Cepis::Error::CEPNotFound, and the faixa hangs off the exception rather than turning a miss into an address. Parsing it is deliberately forgiving — a missing, empty or malformed block yields undef, because losing the clean not-found to a parse failure would be a worse outcome than losing the context. cep_ini and cep_fim are the smallest and largest CEP that actually exist, not the bounds the Correios publish, and the interval between them is sparse. That is why ceps is exposed alongside them and why the docs say twice that the pair is not a list of valid CEPs.
Diffstat (limited to 'lib/Cepis/Faixa.pm')
-rw-r--r--lib/Cepis/Faixa.pm196
1 files changed, 196 insertions, 0 deletions
diff --git a/lib/Cepis/Faixa.pm b/lib/Cepis/Faixa.pm
new file mode 100644
index 0000000..7dabdc3
--- /dev/null
+++ b/lib/Cepis/Faixa.pm
@@ -0,0 +1,196 @@
+package Cepis::Faixa;
+
+use strict;
+use warnings;
+
+use Scalar::Util ();
+
+our $VERSION = '0.2.0';
+
+sub from_json {
+ my ( $class, $data ) = @_;
+
+ return undef unless ref $data eq 'HASH';
+
+ for my $key (qw(localidade cep_ini cep_fim ceps)) {
+ return undef unless defined $data->{$key};
+ return undef if ref $data->{$key};
+ }
+ return undef unless length "$data->{localidade}";
+ for my $key (qw(cep_ini cep_fim)) {
+ return undef unless "$data->{$key}" =~ /\A[0-9]{8}\z/;
+ }
+ return undef unless Scalar::Util::looks_like_number( $data->{ceps} );
+
+ return bless {
+ localidade => "$data->{localidade}",
+ uf => _string( $data->{uf} ),
+ estado => _string( $data->{estado} ),
+ regiao => _string( $data->{regiao} ),
+ ibge => _string( $data->{ibge} ),
+ cep_ini => "$data->{cep_ini}",
+ cep_fim => "$data->{cep_fim}",
+ ceps => int( 0 + $data->{ceps} ),
+ }, $class;
+}
+
+sub localidade { return $_[0]->{localidade} }
+sub uf { return $_[0]->{uf} }
+sub estado { return $_[0]->{estado} }
+sub regiao { return $_[0]->{regiao} }
+sub ibge { return $_[0]->{ibge} }
+sub cep_ini { return $_[0]->{cep_ini} }
+sub cep_fim { return $_[0]->{cep_fim} }
+sub ceps { return $_[0]->{ceps} }
+
+sub label {
+ my ($self) = @_;
+ return length $self->{uf}
+ ? "$self->{localidade}/$self->{uf}"
+ : $self->{localidade};
+}
+
+sub to_hash {
+ my ($self) = @_;
+ return {
+ localidade => $self->{localidade},
+ uf => $self->{uf},
+ estado => $self->{estado},
+ regiao => $self->{regiao},
+ ibge => $self->{ibge},
+ cep_ini => $self->{cep_ini},
+ cep_fim => $self->{cep_fim},
+ ceps => $self->{ceps},
+ };
+}
+
+sub _string {
+ my ($value) = @_;
+ return '' unless defined $value;
+ return '' if ref $value;
+ return "$value";
+}
+
+1;
+
+__END__
+
+=encoding UTF-8
+
+=head1 NAME
+
+Cepis::Faixa - the municipality a CEP that does not exist would belong to
+
+=head1 SYNOPSIS
+
+ my $address = eval { Cepis::lookup('25845000') };
+ if ( !$address && ref $@ && $@->isa('Cepis::Error::CEPNotFound') ) {
+
+ if ( my $faixa = $@->faixa ) {
+ print $faixa->localidade, "\n"; # "Areal"
+ print $faixa->uf, "\n"; # "RJ"
+ print $faixa->ceps, "\n"; # 118
+ printf "%s..%s\n", $faixa->cep_ini, $faixa->cep_fim;
+ # 25845001..25849899
+ }
+ }
+
+=head1 DESCRIPTION
+
+B<This is context for an error, not an address.> The lookup still failed, the
+CEP still does not exist, and L<Cepis::Client/lookup> still throws
+L<Cepis::Error::CEPNotFound>. What this object adds is I<which municipality
+owns the band the CEP fell into> — enough to tell the user "25845000 does not
+exist in Areal/RJ" instead of "not found".
+
+That matters because a failing CEP is rarely random: it is usually someone who
+knows the city and guessed the number, and the classic guess is the C<-000>
+that 957 municipalities never received.
+
+The object is reachable as C<< $err->faixa >> on L<Cepis::Error::CEPNotFound>,
+and is C<undef> whenever the API sent no C<faixa> — the API B<omits> the field
+rather than sending null when the CEP falls in a band no municipality owns.
+
+=head2 The range is not a list of valid CEPs
+
+L</cep_ini> and L</cep_fim> are the B<smallest and the largest CEP that actually
+exist> in that municipality. They are not the bounds the Correios publish: the
+DNE's own start for Areal is C<25845000> — exactly the CEP that just failed.
+
+The range is also B<sparse>. Areal's 118 CEPs are scattered across the roughly
+4,900 numbers between C<25845001> and C<25849899>. Iterating the interval, or
+presenting it as "the valid CEPs for this city", produces mostly numbers that do
+not exist. L</ceps> is there precisely so that cannot pass unnoticed: it says how
+many of them are real.
+
+Field names are the API's own, in Portuguese. See L<Cepis::Address> for why.
+
+=head1 CONSTRUCTOR
+
+=head2 from_json
+
+ my $faixa = Cepis::Faixa->from_json( $hashref );
+
+Builds from the C<faixa> block of a 404 body. Returns C<undef> when the block is
+missing, empty, not a hash, or malformed.
+
+Parsing here is deliberately forgiving: a bad C<faixa> must never turn a clean
+not-found into a parse failure. The caller still gets its
+L<Cepis::Error::CEPNotFound>, just without the extra context.
+
+=head1 ACCESSORS
+
+=head2 localidade
+
+Municipality name. Always present and non-empty — a block without it does not
+build.
+
+=head2 uf
+
+Two-letter state code.
+
+=head2 estado
+
+Full state name.
+
+=head2 regiao
+
+Region of the country, such as C<"Sudeste">.
+
+=head2 ibge
+
+IBGE municipality code. The stable way to identify the municipality; the name
+alone is not unique across states.
+
+=head2 cep_ini
+
+The smallest CEP that exists in the municipality, unmasked. Read the warning
+above before treating it as a lower bound.
+
+=head2 cep_fim
+
+The largest CEP that exists in the municipality, unmasked.
+
+=head2 ceps
+
+How many CEPs the municipality actually has, as an integer. Compare it with the
+span between L</cep_ini> and L</cep_fim> to see how sparse the range is.
+
+=head2 label
+
+C<"Areal/RJ">: the municipality and its state, ready to put in a message. Falls
+back to the municipality alone when the API sent no C<uf>.
+
+=head2 to_hash
+
+The eight fields as a plain hashref.
+
+=head1 SEE ALSO
+
+L<Cepis>, L<Cepis::Error>, L<Cepis::Client>
+
+=head1 LICENCE
+
+ISC. Copyright (c) 2026 Murilo Ijanc E<lt>[email protected]<gt>.
+
+=cut