diff options
| author | Murilo Ijanc <[email protected]> | 2026-07-27 20:27:46 -0300 |
|---|---|---|
| committer | Murilo Ijanc <[email protected]> | 2026-07-27 20:27:46 -0300 |
| commit | 2795e86ccf5a15a7601611aaa12be1e0758c963a (patch) | |
| tree | 6a348960633e3e62eaeb102e74e8d1ceab68d65a /src/main | |
| parent | 329f679cd359bcab92adf3366cf17711836d0c79 (diff) | |
| download | java-2795e86ccf5a15a7601611aaa12be1e0758c963a.tar.gz | |
A CEP that fails is almost never random: it is someone who knows the city
and guessed the number, usually the -000 that 957 municipalities never
received. The API now answers those 404s with a faixa block naming the
municipality that owns the band, so CepNotFoundException exposes it and
says it in its message.
It stays an error. The band is context, not an address, and its cep_ini
and cep_fim are the smallest and largest CEP that actually exist there —
not the bounds the Correios publish, and not a dense interval — so the
type documents both traps rather than inviting callers to walk the range.
Reading it is best effort: a missing, malformed or oddly shaped faixa is
dropped, because turning a definitive answer about the request into a
parse failure would hide the answer.
Diffstat (limited to 'src/main')
| -rw-r--r-- | src/main/java/is/cep/cepis/CepNotFoundException.java | 42 | ||||
| -rw-r--r-- | src/main/java/is/cep/cepis/Cepis.java | 2 | ||||
| -rw-r--r-- | src/main/java/is/cep/cepis/Client.java | 43 | ||||
| -rw-r--r-- | src/main/java/is/cep/cepis/Faixa.java | 87 | ||||
| -rw-r--r-- | src/main/java/is/cep/cepis/Json.java | 4 | ||||
| -rw-r--r-- | src/main/java/is/cep/cepis/Main.java | 2 |
6 files changed, 172 insertions, 8 deletions
diff --git a/src/main/java/is/cep/cepis/CepNotFoundException.java b/src/main/java/is/cep/cepis/CepNotFoundException.java index d6865d7..e641b5d 100644 --- a/src/main/java/is/cep/cepis/CepNotFoundException.java +++ b/src/main/java/is/cep/cepis/CepNotFoundException.java @@ -1,20 +1,60 @@ package is.cep.cepis; +import java.util.Optional; + /** * The CEP is well-formed but does not exist in the DNE database. * * <p>Definitive: the client does not retry it, and neither should you. + * + * <p>{@link #faixa()} may name the municipality the CEP would have belonged + * to. The API sends it because a CEP that fails is rarely random: it is + * usually someone who knows the city and guessed the number — the classic + * guess being the {@code -000}, which 957 municipalities never received. + * + * <p>It is <b>context for the error, not an address</b>. This is still a + * not-found; handing the caller the municipality as though it were the result + * turns a clean failure into a wrong answer. It is empty whenever the CEP fell + * in a band no municipality owns, and the field is simply absent from the + * response then, never null. */ public class CepNotFoundException extends CepisException { private static final long serialVersionUID = 1L; + private final transient Faixa faixa; + /** - * Create the exception. + * Create the exception, with no municipality behind the CEP. * * @param message what went wrong */ public CepNotFoundException(String message) { + this(message, null); + } + + /** + * Create the exception, naming the municipality that owns the band. + * + * @param message what went wrong + * @param faixa the municipality the CEP would have belonged to, or + * {@code null} when no municipality owns that band + */ + public CepNotFoundException(String message, Faixa faixa) { super(message); + this.faixa = faixa; + } + + /** + * The municipality the CEP would have belonged to. + * + * <p>Read {@link Faixa} before showing any of it: the interval it carries + * is sparse, and its start is not the start the Correios publish. + * + * @return the band's municipality, empty when the CEP fell outside every + * municipality's band + */ + public Optional<Faixa> faixa() { + return Optional.ofNullable(faixa); } } diff --git a/src/main/java/is/cep/cepis/Cepis.java b/src/main/java/is/cep/cepis/Cepis.java index fb8b742..8e6c980 100644 --- a/src/main/java/is/cep/cepis/Cepis.java +++ b/src/main/java/is/cep/cepis/Cepis.java @@ -38,7 +38,7 @@ import java.util.List; public final class Cepis { /** The version of this library. */ - public static final String VERSION = "0.1.0"; + public static final String VERSION = "0.2.0"; private Cepis() { } diff --git a/src/main/java/is/cep/cepis/Client.java b/src/main/java/is/cep/cepis/Client.java index afc5705..77dba71 100644 --- a/src/main/java/is/cep/cepis/Client.java +++ b/src/main/java/is/cep/cepis/Client.java @@ -135,11 +135,17 @@ public final class Client { * <p>This is the only call that carries a coordinate. About 57% of CEPs * have one; when they do not, {@link Address#geo()} is {@code null}. * + * <p>A CEP that does not exist throws, as it always did. What is new is + * that the exception may name the municipality whose band the CEP fell in, + * in {@link CepNotFoundException#faixa()} — context for the failure, never + * a substitute for the address. + * * @param cep the CEP, masked or not * @return the address * @throws InvalidCepException if the CEP is not eight digits, in which case * no request is made, or if the API rejects it with a 400 - * @throws CepNotFoundException if it is well-formed but not in the database + * @throws CepNotFoundException if it is well-formed but not in the + * database, possibly carrying the band's municipality * @throws RateLimitedException if the API answered 429 on every attempt * @throws ServerErrorException if the API answered 5xx on every attempt * @throws NetworkException if the request never completed @@ -150,8 +156,8 @@ public final class Client { Response response = request("/" + number, OK_ONLY); if (response.status() == 404) { - throw new CepNotFoundException( - "CEP " + number + " does not exist in the DNE database"); + Faixa faixa = faixaOf(response.body()); + throw new CepNotFoundException(notFoundMessage(number, faixa), faixa); } if (response.status() == 400) { throw new InvalidCepException( @@ -433,6 +439,37 @@ public final class Client { return object; } + /** + * Read the {@code faixa} block a 404 may carry. + * + * <p>Everything here is best effort. A 404 with no {@code faixa}, with a + * {@code faixa} that is not an object, or with a body that is not JSON at + * all is still a plain not-found: this returns {@code null} and never + * throws, because turning a definitive answer about the request into a + * parse failure would hide the real one. + */ + private static Faixa faixaOf(byte[] body) { + try { + Object data = Json.parse(Json.decodeUtf8(body)); + if (data instanceof Map<?, ?> object + && object.get("faixa") instanceof Map<?, ?> block) { + return Faixa.fromJson(block); + } + } catch (CharacterCodingException | RuntimeException unusable) { + return null; + } + return null; + } + + private static String notFoundMessage(String number, Faixa faixa) { + String missing = "CEP " + number + " does not exist in the DNE database"; + if (faixa == null) { + return missing; + } + return missing + " — that band belongs to " + faixa.describe() + ", which has " + + faixa.ceps() + " CEP(s) between " + faixa.cepIni() + " and " + faixa.cepFim(); + } + private static String errorMessage(byte[] body, String fallback) { try { Object data = Json.parse(Json.decodeUtf8(body)); diff --git a/src/main/java/is/cep/cepis/Faixa.java b/src/main/java/is/cep/cepis/Faixa.java new file mode 100644 index 0000000..6f92f6e --- /dev/null +++ b/src/main/java/is/cep/cepis/Faixa.java @@ -0,0 +1,87 @@ +package is.cep.cepis; + +import java.util.Map; +import java.util.Objects; + +/** + * The municipality a CEP that does not exist would have belonged to. + * + * <p>It comes attached to {@link CepNotFoundException#faixa()}, and it is + * <b>context for an error, never an address</b>. The CEP asked for still does + * not exist; this only says which municipality owns the band it fell into. + * Treating it as a result turns a not-found into a wrong answer. + * + * <p>It is worth having because a CEP that fails is rarely random: it is + * usually someone who knows the city and guessed the number. The classic guess + * is the {@code -000}, which 957 municipalities never received. + * + * <p>Two things about {@link #cepIni()} and {@link #cepFim()}: + * + * <ul> + * <li>They are the smallest and the largest CEP that <b>actually exist</b> + * there, not the bounds the Correios publish. The DNE's own start for + * Areal is {@code 25845000} — precisely the CEP that just failed.</li> + * <li>The interval is <b>sparse</b>. Areal's 118 CEPs are spread over some + * 9000 numbers, so the overwhelming majority of what lies between the two + * does not exist. That is what {@link #ceps()} is for. Never present the + * pair as a list, or as a range, of valid CEPs.</li> + * </ul> + * + * @param localidade the municipality that owns the band + * @param uf the two-letter state code + * @param estado the state's name + * @param regiao the region of the country + * @param ibge the IBGE municipality code + * @param cepIni the smallest CEP that exists there, the API's {@code cep_ini} + * @param cepFim the largest CEP that exists there, the API's {@code cep_fim} + * @param ceps how many CEPs exist between the two, the rest of the interval + * being empty + */ +public record Faixa( + String localidade, + String uf, + String estado, + String regiao, + String ibge, + String cepIni, + String cepFim, + long ceps) { + + /** + * Canonical constructor. + * + * @throws NullPointerException if {@code localidade} is {@code null} + */ + public Faixa { + Objects.requireNonNull(localidade, "localidade"); + } + + /** + * How the municipality is usually written, {@code "Areal/RJ"}. + * + * <p>Presentation, and it lives here so it does not become a repeated + * concatenation in every message that reports a CEP nobody has. + * + * @return the municipality and its state, or {@link #localidade()} alone + * when there is no state code + */ + public String describe() { + return uf == null || uf.isEmpty() ? localidade : localidade + "/" + uf; + } + + static Faixa fromJson(Map<?, ?> block) { + String localidade = Json.string(block, "localidade"); + if (localidade.isBlank()) { + throw new IllegalArgumentException("the faixa block has no localidade"); + } + return new Faixa( + localidade, + Json.string(block, "uf"), + Json.string(block, "estado"), + Json.string(block, "regiao"), + Json.string(block, "ibge"), + Json.string(block, "cep_ini"), + Json.string(block, "cep_fim"), + Json.count(block, "ceps")); + } +} diff --git a/src/main/java/is/cep/cepis/Json.java b/src/main/java/is/cep/cepis/Json.java index 9a5f17b..eac5beb 100644 --- a/src/main/java/is/cep/cepis/Json.java +++ b/src/main/java/is/cep/cepis/Json.java @@ -315,7 +315,7 @@ final class Json { * @param key the field name * @return the field's value, or {@code ""} */ - static String string(Map<String, ?> data, String key) { + static String string(Map<?, ?> data, String key) { Object value = data.get(key); if (value instanceof String s) { return s; @@ -333,7 +333,7 @@ final class Json { * @param key the field name * @return the field's value, or {@code 0} when absent or unusable */ - static long count(Map<String, ?> data, String key) { + static long count(Map<?, ?> data, String key) { Object value = data.get(key); if (value instanceof Number n) { return n.longValue(); diff --git a/src/main/java/is/cep/cepis/Main.java b/src/main/java/is/cep/cepis/Main.java index edf2d91..6523b1e 100644 --- a/src/main/java/is/cep/cepis/Main.java +++ b/src/main/java/is/cep/cepis/Main.java @@ -13,7 +13,7 @@ import java.util.List; * instance answers before pointing code at it. * * <pre>{@code - * java -jar cepis-0.1.0.jar --base-url http://localhost:8080 01001000 + * java -jar cepis-0.2.0.jar --base-url http://localhost:8080 01001000 * }</pre> * * <p>A single CEP goes through the unit lookup, which carries the coordinate; |