summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/Cepis.pas232
1 files changed, 227 insertions, 5 deletions
diff --git a/src/Cepis.pas b/src/Cepis.pas
index 7b628e9..cf62d08 100644
--- a/src/Cepis.pas
+++ b/src/Cepis.pas
@@ -16,7 +16,8 @@
Ownership, in one sentence: every object this unit hands back belongs to the
caller, who frees it - except the geo block, which belongs to its address,
- and the default client, which belongs to this unit.
+ the faixa block, which belongs to the not-found error carrying it, and the
+ default client, which belongs to this unit.
Note on field names: the API surface is English, but the data fields keep the
API's own Portuguese names (Logradouro, Bairro, Localidade). What you read in
@@ -36,7 +37,7 @@ uses
const
{ Version of this library, as it appears in the default User-Agent. }
- CepisVersion = '0.1.0';
+ CepisVersion = '0.2.0';
{ Where the API lives when nothing else is configured. }
CepisDefaultURL = 'https://cep.is';
@@ -45,6 +46,9 @@ const
CepisMaxBatch = 500;
type
+ { Declared in full below. ECepisNotFound carries one. }
+ TCepisFaixa = class;
+
{
Base of every error this unit raises. Catch it to handle any failure.
@@ -71,8 +75,48 @@ type
{
The CEP is well formed but does not exist in the DNE database.
+
+ Faixa, when it is there, names the municipality whose CEP band the number
+ falls into - enough to answer "that CEP does not exist in Areal" instead of
+ just "not found". It is context for the failure and nothing more: the CEP
+ asked for still does not exist, and a faixa never turns this into an
+ address.
+
+ It is nil when the number falls in a band no municipality owns, and nil
+ against an API that does not send the field at all. The API omits it rather
+ than sending null, so there is no third state to handle. HasFaixa is the
+ shorthand for the check.
}
- ECepisNotFound = class(ECepis);
+ ECepisNotFound = class(ECepis)
+ private
+ FFaixa: TCepisFaixa;
+ function GetHasFaixa: Boolean;
+ public
+ { Takes ownership of AFaixa, which may be nil. }
+ constructor Create(const AMessage: string; AFaixa: TCepisFaixa);
+ reintroduce;
+
+ { Frees the faixa, if any. }
+ destructor Destroy; override;
+
+ {
+ Hand the faixa over to the caller, who becomes responsible for freeing
+ it. Faixa is nil afterwards. Returns nil when there was none.
+
+ The exception itself is freed when the except block ends, so this is the
+ way to let the faixa outlive the handler.
+ }
+ function ExtractFaixa: TCepisFaixa;
+
+ { True when the API placed the CEP inside a municipality's band. }
+ property HasFaixa: Boolean read GetHasFaixa;
+
+ {
+ The municipality the CEP would belong to, or nil. Owned by this
+ exception; see ExtractFaixa.
+ }
+ property Faixa: TCepisFaixa read FFaixa;
+ end;
{
The request never completed: DNS, refused connection, timeout, TLS.
@@ -174,6 +218,73 @@ type
end;
{
+ The municipality a CEP that does not exist would have belonged to.
+
+ Attached to ECepisNotFound when the failed number falls inside the band the
+ Correios allocated to one municipality. The lookup still failed - this is
+ context for the error, never an address.
+
+ Why the API bothers: a CEP that fails is rarely a random number. It is
+ usually someone who knows the city and guessed the last digits, and the
+ classic guess is the '-000' that 957 municipalities never received.
+
+ CepIni and CepFim are the smallest and the largest CEP that really exist
+ there, not the bounds the Correios publish. Areal's DNE band opens at
+ 25845000 - which is exactly the CEP that just failed - while its CepIni is
+ 25845001.
+
+ The pair is not a list. The range is sparse: Areal's 118 CEPs are scattered
+ across the ~4,900 numbers between 25845001 and 25849899, so iterating from
+ one bound to the other yields forty times more numbers than exist. Ceps is
+ how many there are, and it is there precisely so the gap cannot pass
+ unnoticed.
+
+ Owned by the ECepisNotFound that carries it, and freed with it. Use
+ ECepisNotFound.ExtractFaixa to take ownership.
+ }
+ TCepisFaixa = class(TPersistent)
+ private
+ FLocalidade: string;
+ FUF: string;
+ FEstado: string;
+ FRegiao: string;
+ FIbge: string;
+ FCepIni: string;
+ FCepFim: string;
+ FCeps: Int64;
+ public
+ constructor Create(const ALocalidade, AUF, AEstado, ARegiao, AIbge,
+ ACepIni, ACepFim: string; ACeps: Int64);
+ published
+ { Municipality that owns the band, e.g. 'Areal'. }
+ property Localidade: string read FLocalidade;
+
+ { Two-letter state code, e.g. 'RJ'. }
+ property UF: string read FUF;
+
+ { State name in full, e.g. 'Rio de Janeiro'. }
+ property Estado: string read FEstado;
+
+ { Region of the country, e.g. 'Sudeste'. }
+ property Regiao: string read FRegiao;
+
+ { IBGE municipality code - the key to join against census data. }
+ property Ibge: string read FIbge;
+
+ { Smallest CEP that exists in the municipality, unmasked. }
+ property CepIni: string read FCepIni;
+
+ { Largest CEP that exists in the municipality, unmasked. }
+ property CepFim: string read FCepFim;
+
+ {
+ How many CEPs exist between CepIni and CepFim, both included. Far fewer
+ than the arithmetic distance between them - see the note above.
+ }
+ property Ceps: Int64 read FCeps;
+ end;
+
+ {
A CEP and everything the database knows about it.
Fields the database does not have for that CEP come back as empty strings,
@@ -398,6 +509,10 @@ type
database; ECepisRateLimited, ECepisServerError or ECepisNetworkError once
the attempts are exhausted; ECepisInvalidResponse when a success status
carries a body that is not a JSON object.
+
+ The ECepisNotFound may carry a TCepisFaixa naming the municipality the
+ CEP would have belonged to. Read it there; a not-found is still a
+ not-found.
}
function Lookup(const ACep: string): TCepisAddress;
@@ -783,6 +898,76 @@ begin
Result := TCepisGeo.Create(Lat, Lon, RaioM, Precisao);
end;
+{
+ Read the faixa block of a 404 body, or nil when there is none to read.
+
+ Deliberately tolerant. A body without 'faixa', with a null or a string
+ there, with an empty object, or with nothing that names a municipality all
+ give nil - which is the same thing the caller sees against an API that never
+ sends the field. A field that is there but unreadable falls back to empty,
+ or to 0 for Ceps, rather than throwing the municipality away with it.
+
+ Nothing here raises and nothing here leaks: a faixa is a bonus on top of an
+ error, and no shape of it may cost the caller the clean not-found underneath.
+}
+function FaixaFromJSON(AObject: TJSONObject): TCepisFaixa;
+var
+ Block: TJSONData;
+ Faixa: TJSONObject;
+ Localidade: string;
+begin
+ Result := nil;
+ if AObject = nil then
+ Exit;
+ Block := AObject.Find('faixa');
+ if not (Block is TJSONObject) then
+ Exit;
+ Faixa := TJSONObject(Block);
+ Localidade := JSONString(Faixa, 'localidade');
+ if Localidade = '' then
+ Exit;
+ Result := TCepisFaixa.Create(Localidade,
+ JSONString(Faixa, 'uf'),
+ JSONString(Faixa, 'estado'),
+ JSONString(Faixa, 'regiao'),
+ JSONString(Faixa, 'ibge'),
+ JSONString(Faixa, 'cep_ini'),
+ JSONString(Faixa, 'cep_fim'),
+ JSONInt64(Faixa, 'ceps'));
+end;
+
+{
+ Build the ECepisNotFound for a 404, faixa included when the body carries one.
+
+ The exception is returned rather than raised so the faixa has exactly one
+ owner from the moment it exists.
+}
+function NotFoundFor(const ANumber, ABody: string): ECepisNotFound;
+var
+ Data: TJSONData;
+ Faixa: TCepisFaixa;
+ Text: string;
+begin
+ Faixa := nil;
+ Data := ParseJSONLoose(ABody);
+ try
+ if Data is TJSONObject then
+ Faixa := FaixaFromJSON(TJSONObject(Data));
+ finally
+ Data.Free;
+ end;
+
+ Text := 'CEP ' + ANumber + ' does not exist in the DNE database';
+ if Faixa <> nil then
+ begin
+ Text := Text + ' - it falls in the CEP range of ' + Faixa.Localidade;
+ if Faixa.UF <> '' then
+ Text := Text + '/' + Faixa.UF;
+ end;
+
+ Result := ECepisNotFound.Create(Text, Faixa);
+end;
+
function AddressFromJSON(AObject: TJSONObject; const ARaw: string): TCepisAddress;
begin
Result := TCepisAddress.Create;
@@ -837,6 +1022,44 @@ begin
FStatus := AStatus;
end;
+constructor ECepisNotFound.Create(const AMessage: string;
+ AFaixa: TCepisFaixa);
+begin
+ inherited Create(AMessage);
+ FFaixa := AFaixa;
+end;
+
+destructor ECepisNotFound.Destroy;
+begin
+ FreeAndNil(FFaixa);
+ inherited Destroy;
+end;
+
+function ECepisNotFound.ExtractFaixa: TCepisFaixa;
+begin
+ Result := FFaixa;
+ FFaixa := nil;
+end;
+
+function ECepisNotFound.GetHasFaixa: Boolean;
+begin
+ Result := FFaixa <> nil;
+end;
+
+constructor TCepisFaixa.Create(const ALocalidade, AUF, AEstado, ARegiao,
+ AIbge, ACepIni, ACepFim: string; ACeps: Int64);
+begin
+ inherited Create;
+ FLocalidade := ALocalidade;
+ FUF := AUF;
+ FEstado := AEstado;
+ FRegiao := ARegiao;
+ FIbge := AIbge;
+ FCepIni := ACepIni;
+ FCepFim := ACepFim;
+ FCeps := ACeps;
+end;
+
constructor TCepisGeo.Create(ALat, ALon: Double; ARaioM: Integer;
const APrecisao: string);
begin
@@ -1072,8 +1295,7 @@ begin
Request('/' + Number, 0, Status, Body);
if Status = 404 then
- raise ECepisNotFound.Create('CEP ' + Number +
- ' does not exist in the DNE database');
+ raise NotFoundFor(Number, Body);
if Status = 400 then
raise ECepisInvalidCEP.Create(ErrorMessageOf(Body,
'CEP ' + Number + ' rejected'));