1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
|
#ifndef CEPIS_CLIENT_HPP
#define CEPIS_CLIENT_HPP
#include <chrono>
#include <cstddef>
#include <optional>
#include <string>
#include <string_view>
#include <vector>
#include "cepis/errors.hpp"
#include "cepis/json.hpp"
#include "cepis/models.hpp"
namespace cepis {
/// The library version, as it appears in the default `User-Agent`.
inline constexpr std::string_view kVersion = "0.2.0";
/// Where the public API lives.
inline constexpr std::string_view kDefaultBaseUrl = "https://cep.is";
/// The API's ceiling on one batch request. `Client::lookup_many` chunks at
/// this size on its own; you do not need to.
inline constexpr std::size_t kMaxBatch = 500;
/// Returns the CEP's eight digits, accepting the mask.
///
/// `"01001-000"`, `"01001000"` and `" 01001-000 "` all become `"01001000"`.
///
/// Throws `InvalidCep` unless exactly eight digits remain. This runs **before**
/// any request: a seven-digit CEP is the caller's bug, and spending a round
/// trip for the server to say so wastes both sides' time.
std::string normalize_cep(std::string_view value);
/// The default `User-Agent`, e.g. `cepis-cpp/0.2.0 (+https://cep.is)`.
std::string default_user_agent();
/// How a `Client` behaves. Every field has a working default; in practice only
/// `base_url` changes between environments.
struct Config {
/// Where the API lives. Point it at a local instance (the API repo brings
/// one up on `http://localhost:8080` with `just up`) or at a test server. A
/// trailing slash is stripped, so it cannot produce doubled separators that
/// some proxies normalise and others 404.
std::string base_url{kDefaultBaseUrl};
/// Cap on one attempt — connection plus transfer. libcurl works on whole
/// transfers rather than per socket operation, so this is not a per-read
/// timeout; the cap on the whole call, retries and waits included, is
/// `total_timeout`.
std::chrono::milliseconds timeout{10000};
/// How many attempts in total, counting the first. `1` disables retrying.
/// The default of `3` covers the common case (one transient failure)
/// without turning a real outage into a long wait.
int max_attempts = 3;
/// Base of the exponential wait. The actual wait is drawn from
/// `[0, base * 2^(n-1)]` — *full jitter*. The randomness is not decoration:
/// without it, N clients that fail together come back together and reproduce
/// the spike that took the service down.
std::chrono::milliseconds backoff_base{500};
/// Cap on each individual wait. It applies to a server-sent `Retry-After`
/// too, or a `Retry-After: 3600` from a misconfigured proxy would park the
/// thread for an hour.
std::chrono::milliseconds backoff_max{8000};
/// Cap on the whole call, attempts and waits included. Unset leaves
/// `max_attempts` as the only bound. Set it in code serving a web request:
/// without it the worst case is `max_attempts * timeout` plus the sum of the
/// backoffs.
std::optional<std::chrono::milliseconds> total_timeout;
/// Overrides the `User-Agent`. Empty means `default_user_agent()`, which
/// identifies the library and its version — that is what lets the server
/// diagnose an old client misbehaving.
std::string user_agent;
};
/// Client for the cep.is API.
///
/// Reuse one instance: the configuration is fixed at construction and no method
/// keeps state between calls, so a `const Client&` is safe to share across
/// threads. Each request owns its own connection.
///
/// ```cpp
/// cepis::Config config;
/// config.base_url = "http://localhost:8080";
/// const cepis::Client client(config);
/// const cepis::Address a = client.lookup("01001-000");
/// ```
class Client {
public:
/// Builds a client. Throws `std::invalid_argument` for a `max_attempts`
/// below 1 or a non-positive `timeout`.
explicit Client(Config config = {});
/// The configuration in use, with `base_url` and `user_agent` normalised.
[[nodiscard]] const Config& config() const noexcept { return config_; }
/// Looks up a single CEP.
///
/// Throws `InvalidCep` when the CEP is not eight digits (no request is
/// made) or when the API answers 400; `CepNotFound` when it is well-formed
/// but absent; `RateLimited` on 429 after every attempt; `ServerError` on
/// 5xx after every attempt; `NetworkError` when the request never
/// completed; `InvalidResponse` when a success status carries a body that
/// is not a JSON object.
///
/// A `CepNotFound` often carries `faixa()`, the municipality the CEP would
/// have belonged to. It is context for the error, not an address.
[[nodiscard]] Address lookup(std::string_view cep) const;
/// Looks up many CEPs in one batch.
///
/// Returns a vector of **the same size and in the same order** as the input.
/// A CEP that does not exist is empty at its position — it does not vanish
/// from the vector, and it does not throw. That is the right difference from
/// `lookup`: in a batch of a thousand, one bad CEP must not cost the other
/// nine hundred and ninety-nine.
///
/// A malformed CEP is likewise empty rather than throwing `InvalidCep`. If
/// you need to know *which* one was malformed, validate first with
/// `normalize_cep`.
///
/// Input is chunked into blocks of `kMaxBatch` automatically.
///
/// **Warning: the batch carries no coordinate.** The endpoint behind it is
/// CSV, and the CSV has no `geo` column. Every `Address` returned here has
/// an empty `geo`, including for CEPs that do have one. Need `geo`? Use
/// `lookup`, one at a time.
[[nodiscard]] std::vector<std::optional<Address>> lookup_many(
const std::vector<std::string>& ceps) const;
/// Queries `GET /health`: does the API answer, and with which database.
///
/// Does not throw for an API reporting itself degraded — that arrives in
/// `Health::status`, and checking health is exactly when you want the
/// diagnosis rather than an exception. Network failures still throw.
[[nodiscard]] Health health() const;
private:
struct Response {
int status = 0;
std::string body;
std::optional<double> retry_after;
};
[[nodiscard]] Response request(std::string_view path,
const std::vector<int>& accepted_status) const;
[[nodiscard]] std::vector<std::optional<Address>> lookup_chunk(const std::string* first,
std::size_t count) const;
[[nodiscard]] std::chrono::milliseconds backoff(int attempt,
std::optional<double> retry_after) const;
Config config_;
};
/// Looks a CEP up with the default configuration.
///
/// The free functions share one process-wide `Client`. It is built once
/// because `Client` keeps no state between calls and is safe to share. Anyone
/// needing different settings should build their own.
Address lookup(std::string_view cep);
/// Looks many CEPs up with the default configuration. Read `Client::lookup_many`
/// first, especially the warning that batches carry no coordinate.
std::vector<std::optional<Address>> lookup_many(const std::vector<std::string>& ceps);
/// Queries the API's health with the default configuration.
Health health();
} // namespace cepis
#endif // CEPIS_CLIENT_HPP
|