diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/client.cpp | 408 | ||||
| -rw-r--r-- | src/csv.cpp | 72 | ||||
| -rw-r--r-- | src/csv.hpp | 21 | ||||
| -rw-r--r-- | src/http.cpp | 142 | ||||
| -rw-r--r-- | src/http.hpp | 39 | ||||
| -rw-r--r-- | src/json.cpp | 529 | ||||
| -rw-r--r-- | src/models.cpp | 137 |
7 files changed, 1348 insertions, 0 deletions
diff --git a/src/client.cpp b/src/client.cpp new file mode 100644 index 0000000..ef19da8 --- /dev/null +++ b/src/client.cpp @@ -0,0 +1,408 @@ +#include "cepis/client.hpp" + +#include <algorithm> +#include <cerrno> +#include <chrono> +#include <cmath> +#include <cstdint> +#include <cstdio> +#include <cstdlib> +#include <iterator> +#include <random> +#include <stdexcept> +#include <string> +#include <thread> + +#include "csv.hpp" +#include "http.hpp" + +namespace cepis { +namespace { + +using Clock = std::chrono::steady_clock; + +bool is_retriable(int status) { + return status == 429 || status == 500 || status == 502 || status == 503 || status == 504; +} + +std::string trim_ascii(std::string_view value) { + const auto is_space = [](char c) { + return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == '\v'; + }; + while (!value.empty() && is_space(value.front())) { + value.remove_prefix(1); + } + while (!value.empty() && is_space(value.back())) { + value.remove_suffix(1); + } + return std::string(value); +} + +int month_from_name(std::string_view name) { + static constexpr std::string_view kMonths[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; + for (int i = 0; i < 12; ++i) { + if (name == kMonths[i]) { + return i; + } + } + return -1; +} + +std::int64_t days_from_civil(std::int64_t year, int month, int day) { + year -= month <= 2 ? 1 : 0; + const std::int64_t era = (year >= 0 ? year : year - 399) / 400; + const auto year_of_era = static_cast<std::uint64_t>(year - (era * 400)); + const auto day_of_year = + static_cast<std::uint64_t>((((153 * (month + (month > 2 ? -3 : 9))) + 2) / 5) + day - 1); + const std::uint64_t day_of_era = + (year_of_era * 365) + (year_of_era / 4) - (year_of_era / 100) + day_of_year; + return (era * 146097) + static_cast<std::int64_t>(day_of_era) - 719468; +} + +void skip_spaces(std::string_view& text) { + while (!text.empty() && (text.front() == ' ' || text.front() == '\t')) { + text.remove_prefix(1); + } +} + +std::optional<int> take_int(std::string_view& text, std::size_t max_digits) { + int value = 0; + std::size_t taken = 0; + while (taken < max_digits && !text.empty() && text.front() >= '0' && text.front() <= '9') { + value = (value * 10) + (text.front() - '0'); + text.remove_prefix(1); + ++taken; + } + return taken == 0 ? std::nullopt : std::optional<int>(value); +} + +bool take_char(std::string_view& text, char expected) { + if (text.empty() || text.front() != expected) { + return false; + } + text.remove_prefix(1); + return true; +} + +std::optional<std::int64_t> parse_http_date(const std::string& value) { + const std::size_t comma = value.find(','); + if (comma == std::string::npos) { + return std::nullopt; + } + std::string normalised = value.substr(comma + 1); + std::replace(normalised.begin(), normalised.end(), '-', ' '); + + std::string_view rest(normalised); + skip_spaces(rest); + const std::optional<int> day = take_int(rest, 2); + skip_spaces(rest); + if (!day || rest.size() < 3) { + return std::nullopt; + } + const int month = month_from_name(rest.substr(0, 3)); + rest.remove_prefix(3); + skip_spaces(rest); + const std::optional<int> year = take_int(rest, 4); + skip_spaces(rest); + const std::optional<int> hour = take_int(rest, 2); + if (!take_char(rest, ':')) { + return std::nullopt; + } + const std::optional<int> minute = take_int(rest, 2); + if (!take_char(rest, ':')) { + return std::nullopt; + } + const std::optional<int> second = take_int(rest, 2); + + if (month < 0 || !year || !hour || !minute || !second) { + return std::nullopt; + } + const int full_year = *year < 100 ? *year + (*year < 70 ? 2000 : 1900) : *year; + if (*day < 1 || *day > 31 || *hour > 23 || *minute > 59 || *second > 60) { + return std::nullopt; + } + + const std::int64_t days = days_from_civil(full_year, month + 1, *day); + return (days * 86400) + (static_cast<std::int64_t>(*hour) * 3600) + + (static_cast<std::int64_t>(*minute) * 60) + *second; +} + +std::optional<double> retry_after_seconds(const std::string& header) { + const std::string value = trim_ascii(header); + if (value.empty()) { + return std::nullopt; + } + + errno = 0; + char* end = nullptr; + const double seconds = std::strtod(value.c_str(), &end); + if (errno == 0 && end == value.c_str() + value.size()) { + return std::max(0.0, seconds); + } + + const std::optional<std::int64_t> target = parse_http_date(value); + if (!target) { + return std::nullopt; + } + const auto now = std::chrono::duration_cast<std::chrono::seconds>( + std::chrono::system_clock::now().time_since_epoch()) + .count(); + return std::max(0.0, static_cast<double>(*target - now)); +} + +double jitter(double ceiling) { + static thread_local std::mt19937 engine(std::random_device{}()); + if (ceiling <= 0.0) { + return 0.0; + } + return std::uniform_real_distribution<double>(0.0, ceiling)(engine); +} + +std::string message_from_error_body(const std::string& body, const std::string& fallback) { + const std::optional<Json> parsed = Json::parse(body); + if (parsed) { + if (const Json* message = parsed->find("mensagem"); + message != nullptr && message->is_string() && !message->as_string().empty()) { + return message->as_string(); + } + } + return fallback; +} + +std::string preview(const std::string& body) { + constexpr std::size_t kLimit = 120; + return body.size() <= kLimit ? body : body.substr(0, kLimit) + "..."; +} + +Json parse_object(const std::string& body) { + const std::optional<Json> parsed = Json::parse(body); + if (!parsed) { + throw InvalidResponse( + "response is not valid JSON — something between you and the API is " + "answering instead of it: " + + preview(body)); + } + if (!parsed->is_object()) { + throw InvalidResponse("expected a JSON object, got: " + preview(body)); + } + return *parsed; +} + +} // namespace + +std::string normalize_cep(std::string_view value) { + std::string cleaned; + cleaned.reserve(value.size()); + for (const char c : trim_ascii(value)) { + if (c == '-' || c == '.' || c == ' ') { + continue; + } + cleaned.push_back(c); + } + const bool digits_only = + std::all_of(cleaned.begin(), cleaned.end(), [](char c) { return c >= '0' && c <= '9'; }); + if (cleaned.size() != 8 || !digits_only) { + throw InvalidCep("CEP must be 8 digits, got '" + std::string(value) + "'"); + } + return cleaned; +} + +std::string default_user_agent() { + return "cepis-cpp/" + std::string(kVersion) + " (+https://cep.is)"; +} + +Client::Client(Config config) : config_(std::move(config)) { + if (config_.max_attempts < 1) { + throw std::invalid_argument("max_attempts must be >= 1"); + } + if (config_.timeout <= std::chrono::milliseconds::zero()) { + throw std::invalid_argument("timeout must be > 0"); + } + while (!config_.base_url.empty() && config_.base_url.back() == '/') { + config_.base_url.pop_back(); + } + if (config_.user_agent.empty()) { + config_.user_agent = default_user_agent(); + } +} + +Address Client::lookup(std::string_view cep) const { + const std::string number = normalize_cep(cep); + const Response response = request("/" + number, {200}); + + if (response.status == 404) { + throw CepNotFound("CEP " + number + " does not exist in the DNE database"); + } + if (response.status == 400) { + throw InvalidCep(message_from_error_body(response.body, "CEP " + number + " rejected")); + } + return Address::from_json(parse_object(response.body)); +} + +std::vector<std::optional<Address>> Client::lookup_many( + const std::vector<std::string>& ceps) const { + std::vector<std::optional<Address>> result; + if (ceps.empty()) { + return result; + } + result.reserve(ceps.size()); + for (std::size_t start = 0; start < ceps.size(); start += kMaxBatch) { + const std::size_t count = std::min(kMaxBatch, ceps.size() - start); + std::vector<std::optional<Address>> chunk = lookup_chunk(ceps.data() + start, count); + result.insert(result.end(), std::make_move_iterator(chunk.begin()), + std::make_move_iterator(chunk.end())); + } + return result; +} + +Health Client::health() const { + const Response response = request("/health", {200, 503}); + return Health::from_json(parse_object(response.body)); +} + +std::vector<std::optional<Address>> Client::lookup_chunk(const std::string* first, + std::size_t count) const { + std::string joined; + for (std::size_t i = 0; i < count; ++i) { + if (i != 0) { + joined.push_back(','); + } + joined += first[i]; + } + const Response response = request("/csv?ceps=" + detail::url_escape(joined), {200}); + + const std::vector<std::vector<std::string>> table = detail::parse_csv(response.body); + std::vector<std::optional<Address>> rows; + rows.reserve(count); + + if (!table.empty()) { + const std::vector<std::string>& header = table.front(); + std::size_t localidade_column = std::string::npos; + for (std::size_t i = 0; i < header.size(); ++i) { + if (header[i] == "localidade") { + localidade_column = i; + } + } + + for (std::size_t r = 1; r < table.size(); ++r) { + const std::vector<std::string>& row = table[r]; + const bool found = + localidade_column < row.size() && !trim_ascii(row[localidade_column]).empty(); + if (!found) { + rows.emplace_back(); + continue; + } + Json object = Json::object(); + for (std::size_t i = 0; i < header.size(); ++i) { + object.set(header[i], Json::string(i < row.size() ? row[i] : std::string{})); + } + rows.emplace_back(Address::from_json(object)); + } + } + + rows.resize(count); + return rows; +} + +std::chrono::milliseconds Client::backoff(int attempt, std::optional<double> retry_after) const { + if (retry_after) { + const std::chrono::milliseconds asked{static_cast<long long>(*retry_after * 1000.0)}; + return std::min(asked, config_.backoff_max); + } + const auto base = static_cast<double>(config_.backoff_base.count()); + const double ceiling = + std::min(base * std::pow(2.0, attempt - 1), static_cast<double>(config_.backoff_max.count())); + return std::chrono::milliseconds{static_cast<long long>(jitter(ceiling))}; +} + +Client::Response Client::request(std::string_view path, + const std::vector<int>& accepted_status) const { + const std::string url = config_.base_url + std::string(path); + const std::optional<Clock::time_point> deadline = + config_.total_timeout + ? std::optional<Clock::time_point>(Clock::now() + *config_.total_timeout) + : std::nullopt; + + int last_status = 0; + std::optional<double> last_retry_after; + std::string last_network_error; + + for (int attempt = 1; attempt <= config_.max_attempts; ++attempt) { + if (deadline && Clock::now() >= *deadline) { + break; + } + + detail::HttpRequest http_request; + http_request.url = url; + http_request.user_agent = config_.user_agent; + http_request.timeout = config_.timeout; + detail::HttpResult http_result = detail::perform_get(http_request); + + if (http_result.completed) { + const int status = http_result.status; + const bool accepted = std::find(accepted_status.begin(), accepted_status.end(), status) != + accepted_status.end(); + if (accepted || !is_retriable(status)) { + Response response; + response.status = status; + response.body = std::move(http_result.body); + response.retry_after = retry_after_seconds(http_result.retry_after); + return response; + } + last_status = status; + last_retry_after = retry_after_seconds(http_result.retry_after); + last_network_error.clear(); + } else { + last_status = 0; + last_retry_after.reset(); + last_network_error = std::move(http_result.curl_error); + } + + if (attempt == config_.max_attempts) { + break; + } + + std::chrono::milliseconds wait = backoff(attempt, last_retry_after); + if (deadline) { + const auto remaining = + std::chrono::duration_cast<std::chrono::milliseconds>(*deadline - Clock::now()); + if (remaining <= std::chrono::milliseconds::zero()) { + break; + } + wait = std::min(wait, remaining); + } + std::this_thread::sleep_for(wait); + } + + const std::string suffix = " (after " + std::to_string(config_.max_attempts) + " attempt(s))"; + if (last_status == 429) { + throw RateLimited("rate limit reached at " + url + suffix, last_retry_after); + } + if (last_status != 0) { + throw ServerError("the API answered " + std::to_string(last_status) + " at " + url + suffix, + last_status); + } + throw NetworkError("could not reach " + url + suffix + ": " + + (last_network_error.empty() ? "the call ran out of time before it started" + : last_network_error)); +} + +namespace { + +Client& default_client() { + static Client client{}; + return client; +} + +} // namespace + +Address lookup(std::string_view cep) { return default_client().lookup(cep); } + +std::vector<std::optional<Address>> lookup_many(const std::vector<std::string>& ceps) { + return default_client().lookup_many(ceps); +} + +Health health() { return default_client().health(); } + +} // namespace cepis diff --git a/src/csv.cpp b/src/csv.cpp new file mode 100644 index 0000000..32b9591 --- /dev/null +++ b/src/csv.cpp @@ -0,0 +1,72 @@ +#include "csv.hpp" + +#include <cstddef> + +namespace cepis::detail { + +std::vector<std::vector<std::string>> parse_csv(std::string_view text) { + if (text.compare(0, 3, "\xEF\xBB\xBF") == 0) { + text.remove_prefix(3); + } + + std::vector<std::vector<std::string>> rows; + std::vector<std::string> row; + std::string field; + bool in_quotes = false; + bool row_has_content = false; + + const auto end_row = [&] { + if (!row_has_content) { + field.clear(); + row.clear(); + return; + } + row.push_back(std::move(field)); + field.clear(); + rows.push_back(std::move(row)); + row.clear(); + row_has_content = false; + }; + + for (std::size_t i = 0; i < text.size(); ++i) { + const char c = text[i]; + + if (in_quotes) { + row_has_content = true; + if (c != '"') { + field.push_back(c); + } else if (i + 1 < text.size() && text[i + 1] == '"') { + field.push_back('"'); + ++i; + } else { + in_quotes = false; + } + continue; + } + + switch (c) { + case '"': + row_has_content = true; + in_quotes = true; + break; + case ',': + row_has_content = true; + row.push_back(std::move(field)); + field.clear(); + break; + case '\r': + break; + case '\n': + end_row(); + break; + default: + row_has_content = true; + field.push_back(c); + } + } + + end_row(); + return rows; +} + +} // namespace cepis::detail diff --git a/src/csv.hpp b/src/csv.hpp new file mode 100644 index 0000000..485d8a5 --- /dev/null +++ b/src/csv.hpp @@ -0,0 +1,21 @@ +#ifndef CEPIS_SRC_CSV_HPP +#define CEPIS_SRC_CSV_HPP + +#include <string> +#include <string_view> +#include <vector> + +namespace cepis::detail { + +/// Parses RFC 4180 CSV into rows of fields. +/// +/// Values may contain commas, quotes and newlines when double-quoted, with +/// `""` as the escape — splitting on `,` would corrupt exactly the street +/// names most likely to matter. A leading UTF-8 BOM is dropped, `\r\n` and +/// `\n` both end a record, and a trailing newline does not produce an empty +/// final row. +std::vector<std::vector<std::string>> parse_csv(std::string_view text); + +} // namespace cepis::detail + +#endif // CEPIS_SRC_CSV_HPP diff --git a/src/http.cpp b/src/http.cpp new file mode 100644 index 0000000..409fa6a --- /dev/null +++ b/src/http.cpp @@ -0,0 +1,142 @@ +#include "http.hpp" + +#include <curl/curl.h> + +#include <algorithm> +#include <cctype> +#include <cstddef> +#include <memory> +#include <mutex> +#include <string> + +namespace cepis::detail { +namespace { + +class GlobalInit { + public: + GlobalInit() { curl_global_init(CURL_GLOBAL_DEFAULT); } + ~GlobalInit() { curl_global_cleanup(); } + + GlobalInit(const GlobalInit&) = delete; + GlobalInit& operator=(const GlobalInit&) = delete; + GlobalInit(GlobalInit&&) = delete; + GlobalInit& operator=(GlobalInit&&) = delete; +}; + +void ensure_global_init() { + static const GlobalInit init; + (void)init; +} + +struct EasyDeleter { + void operator()(CURL* handle) const noexcept { curl_easy_cleanup(handle); } +}; + +struct SlistDeleter { + void operator()(curl_slist* list) const noexcept { curl_slist_free_all(list); } +}; + +using EasyHandle = std::unique_ptr<CURL, EasyDeleter>; +using HeaderList = std::unique_ptr<curl_slist, SlistDeleter>; + +std::size_t write_body(char* data, std::size_t size, std::size_t count, void* user_data) { + const std::size_t total = size * count; + static_cast<std::string*>(user_data)->append(data, total); + return total; +} + +std::size_t read_header(char* data, std::size_t size, std::size_t count, void* user_data) { + const std::size_t total = size * count; + const std::string_view line(data, total); + const std::size_t colon = line.find(':'); + if (colon != std::string_view::npos) { + std::string name(line.substr(0, colon)); + std::transform(name.begin(), name.end(), name.begin(), + [](unsigned char c) { return static_cast<char>(std::tolower(c)); }); + if (name == "retry-after") { + std::string_view value = line.substr(colon + 1); + while (!value.empty() && (value.front() == ' ' || value.front() == '\t')) { + value.remove_prefix(1); + } + while (!value.empty() && (value.back() == '\r' || value.back() == '\n' || + value.back() == ' ' || value.back() == '\t')) { + value.remove_suffix(1); + } + *static_cast<std::string*>(user_data) = std::string(value); + } + } + return total; +} + +} // namespace + +HttpResult perform_get(const HttpRequest& request) { + ensure_global_init(); + + HttpResult result; + const EasyHandle handle(curl_easy_init()); + if (!handle) { + result.curl_error = "could not initialise libcurl"; + return result; + } + + HeaderList headers; + const auto add_header = [&headers](const char* value) { + curl_slist* owned = headers.release(); + curl_slist* grown = curl_slist_append(owned, value); + headers.reset(grown != nullptr ? grown : owned); + }; + add_header("Accept: application/json, text/csv;q=0.9, */*;q=0.1"); + add_header("Accept-Encoding: identity"); + + char error_buffer[CURL_ERROR_SIZE] = {}; + const long timeout_ms = static_cast<long>(request.timeout.count()); + + curl_easy_setopt(handle.get(), CURLOPT_URL, request.url.c_str()); + curl_easy_setopt(handle.get(), CURLOPT_HTTPGET, 1L); + curl_easy_setopt(handle.get(), CURLOPT_USERAGENT, request.user_agent.c_str()); + curl_easy_setopt(handle.get(), CURLOPT_HTTPHEADER, headers.get()); + curl_easy_setopt(handle.get(), CURLOPT_WRITEFUNCTION, write_body); + curl_easy_setopt(handle.get(), CURLOPT_WRITEDATA, &result.body); + curl_easy_setopt(handle.get(), CURLOPT_HEADERFUNCTION, read_header); + curl_easy_setopt(handle.get(), CURLOPT_HEADERDATA, &result.retry_after); + curl_easy_setopt(handle.get(), CURLOPT_TIMEOUT_MS, timeout_ms); + curl_easy_setopt(handle.get(), CURLOPT_CONNECTTIMEOUT_MS, timeout_ms); + curl_easy_setopt(handle.get(), CURLOPT_NOSIGNAL, 1L); + curl_easy_setopt(handle.get(), CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(handle.get(), CURLOPT_MAXREDIRS, 5L); + curl_easy_setopt(handle.get(), CURLOPT_ERRORBUFFER, error_buffer); + + const CURLcode code = curl_easy_perform(handle.get()); + if (code != CURLE_OK) { + result.curl_error = error_buffer[0] != '\0' ? error_buffer : curl_easy_strerror(code); + return result; + } + + long status = 0; + curl_easy_getinfo(handle.get(), CURLINFO_RESPONSE_CODE, &status); + result.completed = true; + result.status = static_cast<int>(status); + return result; +} + +std::string url_escape(std::string_view value) { + static constexpr char kHex[] = "0123456789ABCDEF"; + std::string out; + out.reserve(value.size()); + for (const char raw : value) { + const auto c = static_cast<unsigned char>(raw); + const bool unreserved = (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || + (c >= '0' && c <= '9') || c == '-' || c == '.' || c == '_' || c == '~'; + if (unreserved) { + out.push_back(static_cast<char>(c)); + } else { + out.push_back('%'); + out.push_back(kHex[c >> 4]); + out.push_back(kHex[c & 0x0F]); + } + } + return out; +} + +} // namespace cepis::detail diff --git a/src/http.hpp b/src/http.hpp new file mode 100644 index 0000000..601a2d5 --- /dev/null +++ b/src/http.hpp @@ -0,0 +1,39 @@ +#ifndef CEPIS_SRC_HTTP_HPP +#define CEPIS_SRC_HTTP_HPP + +#include <chrono> +#include <string> +#include <string_view> + +namespace cepis::detail { + +/// What one attempt asked for. +struct HttpRequest { + std::string url; + std::string user_agent; + std::chrono::milliseconds timeout{0}; +}; + +/// What one attempt got back. +/// +/// `completed` separates "the server answered, here is its status" from "the +/// request never got there" — the two need different errors, and only the +/// second one has a `curl_error` worth showing. +struct HttpResult { + bool completed = false; + int status = 0; + std::string body; + std::string retry_after; + std::string curl_error; +}; + +/// Performs one GET. Never throws; failures arrive in `HttpResult`. +HttpResult perform_get(const HttpRequest& request); + +/// Percent-encodes `value` for use in a query-string value, RFC 3986 +/// unreserved set. A comma becomes `%2C`. +std::string url_escape(std::string_view value); + +} // namespace cepis::detail + +#endif // CEPIS_SRC_HTTP_HPP diff --git a/src/json.cpp b/src/json.cpp new file mode 100644 index 0000000..4d8bf03 --- /dev/null +++ b/src/json.cpp @@ -0,0 +1,529 @@ +#include "cepis/json.hpp" + +#include <cmath> +#include <cstdint> +#include <cstdio> +#include <cstdlib> +#include <string> + +namespace cepis { +namespace { + +const std::string& empty_string() { + static const std::string value; + return value; +} + +const Json::Array& empty_array() { + static const Json::Array value; + return value; +} + +const Json::Object& empty_object() { + static const Json::Object value; + return value; +} + +void append_utf8(std::string& out, std::uint32_t code_point) { + if (code_point <= 0x7F) { + out.push_back(static_cast<char>(code_point)); + } else if (code_point <= 0x7FF) { + out.push_back(static_cast<char>(0xC0 | (code_point >> 6))); + out.push_back(static_cast<char>(0x80 | (code_point & 0x3F))); + } else if (code_point <= 0xFFFF) { + out.push_back(static_cast<char>(0xE0 | (code_point >> 12))); + out.push_back(static_cast<char>(0x80 | ((code_point >> 6) & 0x3F))); + out.push_back(static_cast<char>(0x80 | (code_point & 0x3F))); + } else { + out.push_back(static_cast<char>(0xF0 | (code_point >> 18))); + out.push_back(static_cast<char>(0x80 | ((code_point >> 12) & 0x3F))); + out.push_back(static_cast<char>(0x80 | ((code_point >> 6) & 0x3F))); + out.push_back(static_cast<char>(0x80 | (code_point & 0x3F))); + } +} + +class Parser { + public: + explicit Parser(std::string_view text) : text_(text) {} + + std::optional<Json> run() { + skip_whitespace(); + std::optional<Json> value = parse_value(0); + if (!value) { + return std::nullopt; + } + skip_whitespace(); + if (pos_ != text_.size()) { + return std::nullopt; + } + return value; + } + + private: + static constexpr int kMaxDepth = 64; + + [[nodiscard]] bool at_end() const { return pos_ >= text_.size(); } + [[nodiscard]] char peek() const { return text_[pos_]; } + + void skip_whitespace() { + while (!at_end()) { + const char c = peek(); + if (c == ' ' || c == '\t' || c == '\n' || c == '\r') { + ++pos_; + } else { + break; + } + } + } + + bool consume(char expected) { + if (at_end() || peek() != expected) { + return false; + } + ++pos_; + return true; + } + + bool consume(std::string_view literal) { + if (text_.compare(pos_, literal.size(), literal) != 0) { + return false; + } + pos_ += literal.size(); + return true; + } + + std::optional<Json> parse_value(int depth) { + if (depth > kMaxDepth || at_end()) { + return std::nullopt; + } + switch (peek()) { + case '{': + return parse_object(depth); + case '[': + return parse_array(depth); + case '"': { + std::optional<std::string> text = parse_string(); + if (!text) { + return std::nullopt; + } + return Json::string(std::move(*text)); + } + case 't': + return consume("true") ? std::optional<Json>{Json::boolean(true)} : std::nullopt; + case 'f': + return consume("false") ? std::optional<Json>{Json::boolean(false)} : std::nullopt; + case 'n': + return consume("null") ? std::optional<Json>{Json{}} : std::nullopt; + default: + return parse_number(); + } + } + + std::optional<Json> parse_object(int depth) { + ++pos_; + Json result = Json::object(); + skip_whitespace(); + if (consume('}')) { + return result; + } + while (true) { + skip_whitespace(); + if (at_end() || peek() != '"') { + return std::nullopt; + } + std::optional<std::string> key = parse_string(); + if (!key) { + return std::nullopt; + } + skip_whitespace(); + if (!consume(':')) { + return std::nullopt; + } + skip_whitespace(); + std::optional<Json> value = parse_value(depth + 1); + if (!value) { + return std::nullopt; + } + result.set(std::move(*key), std::move(*value)); + skip_whitespace(); + if (consume(',')) { + continue; + } + if (consume('}')) { + return result; + } + return std::nullopt; + } + } + + std::optional<Json> parse_array(int depth) { + ++pos_; + Json result = Json::array(); + skip_whitespace(); + if (consume(']')) { + return result; + } + while (true) { + skip_whitespace(); + std::optional<Json> value = parse_value(depth + 1); + if (!value) { + return std::nullopt; + } + result.push_back(std::move(*value)); + skip_whitespace(); + if (consume(',')) { + continue; + } + if (consume(']')) { + return result; + } + return std::nullopt; + } + } + + std::optional<std::string> parse_string() { + ++pos_; + std::string out; + while (true) { + if (at_end()) { + return std::nullopt; + } + const char c = text_[pos_++]; + if (c == '"') { + return out; + } + if (static_cast<unsigned char>(c) < 0x20) { + return std::nullopt; + } + if (c != '\\') { + out.push_back(c); + continue; + } + if (at_end()) { + return std::nullopt; + } + const char escape = text_[pos_++]; + switch (escape) { + case '"': + out.push_back('"'); + break; + case '\\': + out.push_back('\\'); + break; + case '/': + out.push_back('/'); + break; + case 'b': + out.push_back('\b'); + break; + case 'f': + out.push_back('\f'); + break; + case 'n': + out.push_back('\n'); + break; + case 'r': + out.push_back('\r'); + break; + case 't': + out.push_back('\t'); + break; + case 'u': { + std::optional<std::uint32_t> unit = parse_hex4(); + if (!unit) { + return std::nullopt; + } + std::uint32_t code_point = *unit; + if (code_point >= 0xD800 && code_point <= 0xDBFF) { + if (text_.compare(pos_, 2, "\\u") == 0) { + const std::size_t saved = pos_; + pos_ += 2; + std::optional<std::uint32_t> low = parse_hex4(); + if (low && *low >= 0xDC00 && *low <= 0xDFFF) { + code_point = 0x10000 + ((code_point - 0xD800) << 10) + (*low - 0xDC00); + } else { + pos_ = saved; + code_point = 0xFFFD; + } + } else { + code_point = 0xFFFD; + } + } else if (code_point >= 0xDC00 && code_point <= 0xDFFF) { + code_point = 0xFFFD; + } + append_utf8(out, code_point); + break; + } + default: + return std::nullopt; + } + } + } + + std::optional<std::uint32_t> parse_hex4() { + if (pos_ + 4 > text_.size()) { + return std::nullopt; + } + std::uint32_t value = 0; + for (int i = 0; i < 4; ++i) { + const char c = text_[pos_++]; + value <<= 4; + if (c >= '0' && c <= '9') { + value |= static_cast<std::uint32_t>(c - '0'); + } else if (c >= 'a' && c <= 'f') { + value |= static_cast<std::uint32_t>(c - 'a' + 10); + } else if (c >= 'A' && c <= 'F') { + value |= static_cast<std::uint32_t>(c - 'A' + 10); + } else { + return std::nullopt; + } + } + return value; + } + + std::optional<Json> parse_number() { + const std::size_t start = pos_; + if (!at_end() && peek() == '-') { + ++pos_; + } + if (at_end() || !is_digit(peek())) { + return std::nullopt; + } + if (peek() == '0') { + ++pos_; + } else { + while (!at_end() && is_digit(peek())) { + ++pos_; + } + } + if (!at_end() && peek() == '.') { + ++pos_; + if (at_end() || !is_digit(peek())) { + return std::nullopt; + } + while (!at_end() && is_digit(peek())) { + ++pos_; + } + } + if (!at_end() && (peek() == 'e' || peek() == 'E')) { + ++pos_; + if (!at_end() && (peek() == '+' || peek() == '-')) { + ++pos_; + } + if (at_end() || !is_digit(peek())) { + return std::nullopt; + } + while (!at_end() && is_digit(peek())) { + ++pos_; + } + } + const std::string literal(text_.substr(start, pos_ - start)); + return Json::number(std::strtod(literal.c_str(), nullptr)); + } + + static bool is_digit(char c) { return c >= '0' && c <= '9'; } + + std::string_view text_; + std::size_t pos_ = 0; +}; + +void dump_string(const std::string& value, std::string& out) { + out.push_back('"'); + for (const char c : value) { + switch (c) { + case '"': + out += "\\\""; + break; + case '\\': + out += "\\\\"; + break; + case '\b': + out += "\\b"; + break; + case '\f': + out += "\\f"; + break; + case '\n': + out += "\\n"; + break; + case '\r': + out += "\\r"; + break; + case '\t': + out += "\\t"; + break; + default: + if (static_cast<unsigned char>(c) < 0x20) { + char buffer[7]; + std::snprintf(buffer, sizeof(buffer), "\\u%04x", static_cast<unsigned char>(c)); + out += buffer; + } else { + out.push_back(c); + } + } + } + out.push_back('"'); +} + +void dump_number(double value, std::string& out) { + if (!std::isfinite(value)) { + out += "null"; + return; + } + if (value == static_cast<double>(static_cast<std::int64_t>(value)) && std::fabs(value) < 9.0e15) { + out += std::to_string(static_cast<std::int64_t>(value)); + return; + } + char buffer[40]; + for (const int digits : {15, 16, 17}) { + std::snprintf(buffer, sizeof(buffer), "%.*g", digits, value); + if (std::strtod(buffer, nullptr) == value) { + break; + } + } + out += buffer; +} + +void dump_value(const Json& value, std::string& out) { + switch (value.kind()) { + case Json::Kind::Null: + out += "null"; + break; + case Json::Kind::Bool: + out += value.as_bool() ? "true" : "false"; + break; + case Json::Kind::Number: + dump_number(value.as_number(), out); + break; + case Json::Kind::String: + dump_string(value.as_string(), out); + break; + case Json::Kind::Array: { + out.push_back('['); + bool first = true; + for (const Json& element : value.as_array()) { + if (!first) { + out.push_back(','); + } + first = false; + dump_value(element, out); + } + out.push_back(']'); + break; + } + case Json::Kind::Object: { + out.push_back('{'); + bool first = true; + for (const auto& [key, member] : value.as_object()) { + if (!first) { + out.push_back(','); + } + first = false; + dump_string(key, out); + out.push_back(':'); + dump_value(member, out); + } + out.push_back('}'); + break; + } + } +} + +} // namespace + +Json Json::boolean(bool value) { + Json result; + result.kind_ = Kind::Bool; + result.bool_ = value; + return result; +} + +Json Json::number(double value) { + Json result; + result.kind_ = Kind::Number; + result.number_ = value; + return result; +} + +Json Json::string(std::string value) { + Json result; + result.kind_ = Kind::String; + result.string_ = std::move(value); + return result; +} + +Json Json::array(Array value) { + Json result; + result.kind_ = Kind::Array; + result.array_ = std::move(value); + return result; +} + +Json Json::object(Object value) { + Json result; + result.kind_ = Kind::Object; + result.object_ = std::move(value); + return result; +} + +std::optional<Json> Json::parse(std::string_view text) { return Parser(text).run(); } + +std::string Json::dump() const { + std::string out; + dump_value(*this, out); + return out; +} + +bool Json::as_bool(bool fallback) const noexcept { return kind_ == Kind::Bool ? bool_ : fallback; } + +double Json::as_number(double fallback) const noexcept { + return kind_ == Kind::Number ? number_ : fallback; +} + +const std::string& Json::as_string() const noexcept { + return kind_ == Kind::String ? string_ : empty_string(); +} + +const Json::Array& Json::as_array() const noexcept { + return kind_ == Kind::Array ? array_ : empty_array(); +} + +const Json::Object& Json::as_object() const noexcept { + return kind_ == Kind::Object ? object_ : empty_object(); +} + +const Json* Json::find(std::string_view key) const noexcept { + if (kind_ != Kind::Object) { + return nullptr; + } + for (const auto& member : object_) { + if (member.first == key) { + return &member.second; + } + } + return nullptr; +} + +void Json::set(std::string key, Json value) { + if (kind_ != Kind::Object) { + kind_ = Kind::Object; + object_.clear(); + } + for (auto& member : object_) { + if (member.first == key) { + member.second = std::move(value); + return; + } + } + object_.emplace_back(std::move(key), std::move(value)); +} + +void Json::push_back(Json value) { + if (kind_ != Kind::Array) { + kind_ = Kind::Array; + array_.clear(); + } + array_.push_back(std::move(value)); +} + +} // namespace cepis diff --git a/src/models.cpp b/src/models.cpp new file mode 100644 index 0000000..84e03f9 --- /dev/null +++ b/src/models.cpp @@ -0,0 +1,137 @@ +#include "cepis/models.hpp" + +#include <cerrno> +#include <cmath> +#include <cstdlib> +#include <limits> +#include <optional> +#include <string> + +namespace cepis { +namespace { + +std::string as_field(const Json* value) { + if (value == nullptr) { + return {}; + } + switch (value->kind()) { + case Json::Kind::String: + return value->as_string(); + case Json::Kind::Number: { + const double number = value->as_number(); + if (number == std::trunc(number) && std::fabs(number) < 9.0e15) { + return std::to_string(static_cast<std::int64_t>(number)); + } + return Json::number(number).dump(); + } + case Json::Kind::Bool: + return value->as_bool() ? "true" : "false"; + default: + return {}; + } +} + +std::optional<double> as_double(const Json* value) { + if (value == nullptr) { + return std::nullopt; + } + if (value->is_number()) { + return value->as_number(); + } + if (!value->is_string()) { + return std::nullopt; + } + const std::string& text = value->as_string(); + if (text.empty()) { + return std::nullopt; + } + errno = 0; + char* end = nullptr; + const double parsed = std::strtod(text.c_str(), &end); + if (errno != 0 || end != text.c_str() + text.size() || !std::isfinite(parsed)) { + return std::nullopt; + } + return parsed; +} + +std::optional<std::int64_t> as_integer(const Json* value) { + if (value == nullptr) { + return std::nullopt; + } + if (value->is_number()) { + const double number = value->as_number(); + if (!std::isfinite(number) || std::fabs(number) >= 9.0e18) { + return std::nullopt; + } + return static_cast<std::int64_t>(std::trunc(number)); + } + if (!value->is_string()) { + return std::nullopt; + } + const std::string& text = value->as_string(); + if (text.empty()) { + return std::nullopt; + } + errno = 0; + char* end = nullptr; + const long long parsed = std::strtoll(text.c_str(), &end, 10); + if (errno != 0 || end != text.c_str() + text.size()) { + return std::nullopt; + } + return static_cast<std::int64_t>(parsed); +} + +std::optional<Geo> geo_from_json(const Json* block) { + if (block == nullptr || !block->is_object()) { + return std::nullopt; + } + const std::optional<double> lat = as_double(block->find("lat")); + const std::optional<double> lon = as_double(block->find("lon")); + const std::optional<std::int64_t> raio_m = as_integer(block->find("raio_m")); + const Json* precisao = block->find("precisao"); + if (!lat || !lon || !raio_m || precisao == nullptr || !precisao->is_string()) { + return std::nullopt; + } + return Geo{*lat, *lon, *raio_m, precisao->as_string()}; +} + +} // namespace + +std::string Address::formatted_cep() const { + if (cep.size() != 8) { + return cep; + } + return cep.substr(0, 5) + "-" + cep.substr(5); +} + +Address Address::from_json(const Json& data) { + Address address; + address.cep = as_field(data.find("cep")); + address.logradouro = as_field(data.find("logradouro")); + address.complemento = as_field(data.find("complemento")); + address.unidade = as_field(data.find("unidade")); + address.bairro = as_field(data.find("bairro")); + address.localidade = as_field(data.find("localidade")); + address.uf = as_field(data.find("uf")); + address.estado = as_field(data.find("estado")); + address.regiao = as_field(data.find("regiao")); + address.ibge = as_field(data.find("ibge")); + address.gia = as_field(data.find("gia")); + address.ddd = as_field(data.find("ddd")); + address.siafi = as_field(data.find("siafi")); + address.geo = geo_from_json(data.find("geo")); + address.raw = data; + return address; +} + +Health Health::from_json(const Json& data) { + Health health; + health.status = as_field(data.find("status")); + health.dne = as_field(data.find("dne")); + health.ceps = as_integer(data.find("ceps")).value_or(0); + health.geo = as_integer(data.find("geo")).value_or(0); + health.base_gerada_em = as_field(data.find("base_gerada_em")); + return health; +} + +} // namespace cepis |