summaryrefslogtreecommitdiffstats
path: root/src/Cepis/CepisClient.cs
diff options
context:
space:
mode:
Diffstat (limited to 'src/Cepis/CepisClient.cs')
-rw-r--r--src/Cepis/CepisClient.cs70
1 files changed, 67 insertions, 3 deletions
diff --git a/src/Cepis/CepisClient.cs b/src/Cepis/CepisClient.cs
index 72637c0..e32b78e 100644
--- a/src/Cepis/CepisClient.cs
+++ b/src/Cepis/CepisClient.cs
@@ -54,7 +54,7 @@ public sealed class CepisClient : IDisposable
public const int MaxBatch = 500;
/// <summary>The version of this library.</summary>
- public const string Version = "0.1.0";
+ public const string Version = "0.2.0";
private const string AcceptHeader = "application/json, text/csv;q=0.9, */*;q=0.1";
@@ -175,9 +175,17 @@ public sealed class CepisClient : IDisposable
/// Look up a single CEP.
/// </summary>
/// <remarks>
+ /// <para>
/// This is the only call that carries a coordinate. About 57% of CEPs have
/// one; when they do not, <see cref="Address.Geo"/> is
/// <see langword="null"/>.
+ /// </para>
+ /// <para>
+ /// A CEP that does not exist throws, as it always has. What is new is that
+ /// the exception may name the municipality the number would belong to, in
+ /// <see cref="CepNotFoundException.Faixa"/> — context for the failure, not
+ /// an address, and never a reason to treat the lookup as having succeeded.
+ /// </para>
/// </remarks>
/// <param name="cep">the CEP, masked or not</param>
/// <param name="cancellationToken">cancels the call, waits included</param>
@@ -186,7 +194,10 @@ public sealed class CepisClient : IDisposable
/// the CEP is not eight digits, in which case no request is made, or the API
/// rejected it with a 400
/// </exception>
- /// <exception cref="CepNotFoundException">well-formed, but not in the database</exception>
+ /// <exception cref="CepNotFoundException">
+ /// well-formed, but not in the database; carries the municipality it would
+ /// belong to when the API names one
+ /// </exception>
/// <exception cref="RateLimitedException">the API answered 429 on every attempt</exception>
/// <exception cref="ServerErrorException">the API answered 5xx on every attempt</exception>
/// <exception cref="NetworkException">the request never completed</exception>
@@ -198,7 +209,8 @@ public sealed class CepisClient : IDisposable
if (reply.Status == 404)
{
- throw new CepNotFoundException($"CEP {number} does not exist in the DNE database");
+ var faixa = ReadFaixa(reply.Body);
+ throw new CepNotFoundException(NotFoundMessage(number, faixa), faixa);
}
if (reply.Status == 400)
@@ -459,6 +471,58 @@ public sealed class CepisClient : IDisposable
return fallback;
}
+ /// <summary>
+ /// Read the <c>faixa</c> block out of a 404 body, when it has one.
+ /// </summary>
+ /// <remarks>
+ /// The block is optional and always will be: the API omits it — it does not
+ /// send a null — for a CEP in a band no municipality owns. Absent,
+ /// misshapen or unreadable, the answer here is <see langword="null"/> and
+ /// the caller still gets the plain not-found it was going to get anyway.
+ /// </remarks>
+ /// <param name="body">the 404 body</param>
+ /// <returns>the municipality the CEP would belong to, or null</returns>
+ private static Faixa? ReadFaixa(byte[] body)
+ {
+ try
+ {
+ using var document = JsonDocument.Parse(new ReadOnlyMemory<byte>(body));
+ if (document.RootElement.ValueKind == JsonValueKind.Object
+ && document.RootElement.TryGetProperty("faixa", out var faixa))
+ {
+ return FaixaReader.Read(faixa);
+ }
+ }
+ catch (JsonException)
+ {
+ return null;
+ }
+
+ return null;
+ }
+
+ /// <summary>
+ /// What to tell the caller about a CEP that is not in the database.
+ /// </summary>
+ /// <remarks>
+ /// The municipality goes in the message when the API named one, because the
+ /// message is what ends up in a log, and "the range belongs to Areal/RJ" is
+ /// the difference between a dead end and a question worth asking the user.
+ /// The count is in it for the same reason the API sends it: so nobody reads
+ /// the two ends as a list of CEPs to walk.
+ /// </remarks>
+ /// <param name="cep">the eight digits</param>
+ /// <param name="faixa">the municipality it would belong to, if any</param>
+ /// <returns>the message</returns>
+ private static string NotFoundMessage(string cep, Faixa? faixa)
+ {
+ var message = $"CEP {cep} does not exist in the DNE database";
+ return faixa is null
+ ? message
+ : message + $" — the range belongs to {faixa.Localidade}/{faixa.UF}, "
+ + $"which has {faixa.Ceps} CEP(s) between {faixa.CepIni} and {faixa.CepFim}";
+ }
+
private static string Preview(byte[] body) =>
Encoding.UTF8.GetString(body, 0, Math.Min(body.Length, 120));