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