summaryrefslogtreecommitdiffstats
path: root/lib/Cepis.pm
blob: 1ea5b005a5614d9173bf23772205c9ba7fe8827f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
package Cepis;

use strict;
use warnings;

use Exporter 5.57 qw(import);

use Cepis::Address;
use Cepis::Client;
use Cepis::Error;
use Cepis::Faixa;
use Cepis::Geo;
use Cepis::Health;

our $VERSION = '0.2.0';

our @EXPORT_OK = qw(lookup lookup_many health normalize_cep);

my $DEFAULT_CLIENT;

sub default_client {
    $DEFAULT_CLIENT = Cepis::Client->new unless defined $DEFAULT_CLIENT;
    return $DEFAULT_CLIENT;
}

sub lookup {
    return default_client()->lookup(@_);
}

sub lookup_many {
    return default_client()->lookup_many(@_);
}

sub health {
    return default_client()->health;
}

sub normalize_cep {
    return Cepis::Client::normalize_cep(@_);
}

1;

__END__

=encoding UTF-8

=head1 NAME

Cepis - Perl client for the public CEP API at cep.is

=head1 SYNOPSIS

    use Cepis;

    my $address = Cepis::lookup('01001-000');
    print $address->localidade, ' ', $address->uf, "\n";    # São Paulo SP

    my $result = Cepis::lookup_many( [ '01001000', '99999999' ] );
    print defined $result->[1] ? 'found' : 'not found', "\n";

    my $health = Cepis::health();
    print $health->dne, "\n";

=head1 DESCRIPTION

Looks Brazilian postal codes up against L<https://cep.is>: official Correios
data (DNE), refreshed every two weeks. Single lookups carry coordinates when the
database has them; batches resolve up to 500 CEPs in one request.

B<No non-core dependencies.> L<HTTP::Tiny>, L<JSON::PP> and L<Encode> all ship
with Perl, and the CSV reader is part of this distribution. Reaching
C<https://cep.is> does need L<IO::Socket::SSL> and L<Net::SSLeay>, which do not
ship with Perl — see L</"HTTPS support">.

For a script, the module-level functions are enough. In an application, build a
L<Cepis::Client> once and reuse it:

    my $client = Cepis::Client->new( timeout => 5.0, total_timeout => 15.0 );
    my $address = $client->lookup('01001000');

Pointing at a local instance (the API repo brings one up with C<just up>):

    my $client = Cepis::Client->new( base_url => 'http://localhost:8080' );

=head2 Errors

Every failure is thrown as an object under L<Cepis::Error>, so one test catches
the lot, and the split below it is by whether retrying helps:

    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;
        }
    }

A C<CEPNotFound> often knows B<where> the CEP would have been. When the API says
so, C<< $err->faixa >> is a L<Cepis::Faixa> naming the municipality that owns
that band, and the message names it too:

    # "CEP 25845000 does not exist in the DNE database (it would fall in Areal/RJ)"

It is still a failure — C<faixa> is context for the error, never an address —
and it is C<undef> when the CEP fell in a band no municipality owns. It is worth
checking 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.

=head2 Field names

The API surface is English, but the B<data fields keep the API's own Portuguese
names> — C<logradouro>, C<bairro>, C<localidade>. What you read in the JSON is
what you read on the object. See L<Cepis::Address>.

=head2 HTTPS support

L<HTTP::Tiny> speaks HTTPS only when L<IO::Socket::SSL> 1.968 or later and
L<Net::SSLeay> 1.49 or later are installed, and neither ships with Perl. Without
them, any call against the default C<https://cep.is> throws
L<Cepis::Error::Network> straight away with a message naming both modules.
C<Cepis::Client::tls_available()> reports the same thing without making a
request. See L<Cepis::Client/"HTTPS support">.

=head1 FUNCTIONS

The module-level functions all go through one shared default client, built on
first use. It applies process-wide, including to third-party libraries that also
use this module, so do not reconfigure it — anyone needing different settings
should build their own L<Cepis::Client>.

Nothing is exported by default. C<lookup>, C<lookup_many>, C<health> and
C<normalize_cep> are available on request:

    use Cepis qw(lookup lookup_many);

=head2 lookup

    my $address = Cepis::lookup('01001-000');

Looks up a CEP using the default configuration and returns a
L<Cepis::Address>. Shorthand for C<< Cepis::default_client->lookup($cep) >>; see
L<Cepis::Client/lookup> for the errors it throws.

=head2 lookup_many

    my $result = Cepis::lookup_many( \@ceps );

Looks many CEPs up in one batch, using the default configuration, and returns an
arrayref of the same length and order with C<undef> for the misses. Shorthand
for L<Cepis::Client/lookup_many> — read that one, especially the warning that
batches carry no coordinate.

=head2 health

    my $health = Cepis::health();

Queries the API's health using the default configuration and returns a
L<Cepis::Health>. Shorthand for L<Cepis::Client/health>.

=head2 normalize_cep

    my $number = Cepis::normalize_cep('01001-000');    # "01001000"

Strips the mask and validates that 8 digits remain, throwing
L<Cepis::Error::InvalidCEP> otherwise. Makes no request. See
L<Cepis::Client/normalize_cep>.

=head2 default_client

    my $client = Cepis::default_client();

The L<Cepis::Client> behind the functions above, built on first use.

=head1 SEE ALSO

L<Cepis::Client>, L<Cepis::Address>, L<Cepis::Geo>, L<Cepis::Health>,
L<Cepis::Error>, L<Cepis::Faixa>

The API itself is documented at L<https://cep.is>.

=head1 AUTHOR

Murilo Ijanc E<lt>[email protected]<gt>

=head1 LICENCE

ISC. Copyright (c) 2026 Murilo Ijanc E<lt>[email protected]<gt>.

=cut