From 218bc8fbfbbdc2520130ef43652a6cd22a6e67fb Mon Sep 17 00:00:00 2001 From: Murilo Ijanc Date: Mon, 27 Jul 2026 20:42:18 -0300 Subject: Carry the municipality range on a not-found CEP MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A CEP that fails a lookup is rarely a random number. It is usually someone who knows the city and guessed the digits, and the classic guess is the -000 that 957 municipalities never received. The API now answers those 404s with a faixa naming the municipality whose band the number falls into, so a caller can ask "did you mean somewhere in Areal/RJ?" instead of returning a bare "not found". Cep_not_found becomes an inline record so the context rides along with the message, matching how Rate_limited and Server_error already carry theirs. It is still a 404: faixa is context on the failure and never promotes it to an address, and its bounds are the smallest and largest CEP that actually exist there — sparse across the interval, which is what the ceps count is for — not the bounds the Correios publish. Parsing is deliberately lenient. A missing, null, malformed or municipality-less faixa yields None rather than an error: a 404 is a clean answer, and no shape of this field should turn it into an Invalid_response. --- test/test_cepis.ml | 67 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 66 insertions(+), 1 deletion(-) (limited to 'test') diff --git a/test/test_cepis.ml b/test/test_cepis.ml index cc98c4a..a1c575f 100644 --- a/test/test_cepis.ml +++ b/test/test_cepis.ml @@ -180,6 +180,15 @@ let is_error name predicate = function | Ok _ -> check name false | Error e -> check name (predicate e) +let contains haystack needle = + let n = String.length haystack and m = String.length needle in + let rec go i = + if i + m > n then false + else if String.sub haystack i m = needle then true + else go (i + 1) + in + go 0 + let test_normalisation () = List.iter (fun value -> @@ -262,11 +271,65 @@ let test_not_found () = with_server (fun srv client -> respond srv "/99999999" [ reply 404 {|{"erro":"cep_nao_encontrado","mensagem":"gone"}|} ]; + let result = Cepis.Client.lookup client "99999999" in is_error "404 becomes Cep_not_found" (function Cepis.Cep_not_found _ -> true | _ -> false) - (Cepis.Client.lookup client "99999999"); + result; + is_error "a 404 without faixa yields None" + (function + | Cepis.Cep_not_found { faixa = None; _ } -> true | _ -> false) + result; check "404 is not retried" (count srv "/99999999" = 1)) +let areal_404 = + {|{"erro":"cep_nao_encontrado","mensagem":"CEP não existe na base do DNE","faixa":{"localidade":"Areal","uf":"RJ","estado":"Rio de Janeiro","regiao":"Sudeste","ibge":"3300225","cep_ini":"25845001","cep_fim":"25849899","ceps":118}}|} + +let test_not_found_with_faixa () = + with_server (fun srv client -> + respond srv "/25845000" [ reply 404 areal_404 ]; + match Cepis.Client.lookup client "25845000" with + | Ok _ -> check "a faixa does not turn a 404 into an address" false + | Error (Cepis.Cep_not_found { faixa = Some f; _ } as e) -> + check "faixa names the municipality" (f.Cepis.localidade = "Areal"); + check "faixa carries the uf" (f.Cepis.uf = "RJ"); + check "faixa carries the ibge code" (f.Cepis.ibge = "3300225"); + check "faixa carries the region" (f.Cepis.regiao = "Sudeste"); + check "faixa bounds are the CEPs that exist, not the DNE's" + (f.Cepis.cep_ini = "25845001" && f.Cepis.cep_fim = "25849899"); + check "faixa counts how sparse the range is" (f.Cepis.ceps = 118); + check "the rendered error mentions the municipality" + (contains (Cepis.string_of_error e) "Areal"); + check "the rendered error still says not found" + (contains (Cepis.string_of_error e) "CEP not found") + | Error _ -> check "a 404 with faixa is still Cep_not_found" false) + +let test_not_found_faixa_is_defensive () = + with_server (fun srv client -> + List.iter + (fun (path, body, label) -> + respond srv path [ reply 404 body ]; + is_error + (Printf.sprintf "%s yields no faixa, never a parse failure" label) + (function + | Cepis.Cep_not_found { faixa = None; _ } -> true | _ -> false) + (Cepis.Client.lookup client (String.sub path 1 8))) + [ + ( "/25845002", + {|{"erro":"cep_nao_encontrado","faixa":null}|}, + "an explicit null" ); + ( "/25845003", + {|{"erro":"cep_nao_encontrado","faixa":{}}|}, + "an empty object" ); + ( "/25845004", + {|{"erro":"cep_nao_encontrado","faixa":"Areal"}|}, + "a bare string" ); + ( "/25845005", + {|{"erro":"cep_nao_encontrado","faixa":{"uf":"RJ","ceps":"muitos"}}|}, + "a faixa with no municipality" ); + ("/25845006", "captive portal", "a body that is not JSON"); + ("/25845007", "", "an empty body"); + ]) + let test_local_validation_makes_no_request () = with_server (fun srv client -> is_error "malformed CEP is rejected locally" @@ -544,6 +607,8 @@ let () = test_malformed_geo (); test_unknown_field (); test_not_found (); + test_not_found_with_faixa (); + test_not_found_faixa_is_defensive (); test_local_validation_makes_no_request (); test_api_400 (); test_not_json (); -- cgit v1.3