diff options
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; |