diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/Cepis.pas | 232 |
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')); |