diff options
Diffstat (limited to 'lib/Cepis/Faixa.pm')
| -rw-r--r-- | lib/Cepis/Faixa.pm | 196 |
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 |