summaryrefslogtreecommitdiffstats
path: root/lib/Cepis/Faixa.pm
diff options
context:
space:
mode:
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