module; #include #include #include export module routemon:rwgps; import std; import :http.client; import :log; namespace beast = boost::beast; namespace bhttp = beast::http; namespace net = boost::asio; namespace json = boost::json; namespace routemon::rwgps { export struct route_summary { std::int64_t id; std::int64_t user_id; std::string url; std::string name; std::string description; }; struct pagination { std::size_t record_count; std::size_t page_count; std::size_t page_size; std::optional next_page_url; }; struct get_routes_meta { pagination pagination; }; struct get_routes_response { std::vector routes; get_routes_meta meta; }; auto tag_invoke(json::value_to_tag const&, json::value const& jv) -> route_summary { return { .id = json::value_to(jv.at("id")), .user_id = json::value_to(jv.at("user_id")), .url = json::value_to(jv.at("url")), .name = json::value_to(jv.at("name")), .description = json::value_to(jv.at("description")), }; } auto tag_invoke(json::value_to_tag const&, json::value const& jv) -> pagination { return { .record_count = json::value_to(jv.at("record_count")), .page_count = json::value_to(jv.at("page_count")), .page_size = json::value_to(jv.at("page_size")), .next_page_url = json::value_to>(jv.at("next_page_url")), }; } auto tag_invoke(json::value_to_tag const&, json::value const& jv) -> get_routes_meta { return { .pagination = json::value_to(jv.at("pagination")), }; } auto tag_invoke(json::value_to_tag const&, json::value const& jv) -> get_routes_response { return { .routes = json::value_to>(jv.at("routes")), .meta = json::value_to(jv.at("meta")), }; } auto json_value_to_get_routes_response(json::value const& jv) -> get_routes_response { return json::value_to(jv); } export class client { log::logger l_; http::client hc_; std::string api_key_; std::string auth_token_; static constexpr std::string host = "ridewithgps.com"; // TODO: handle failure appropriately auto get_routes_page(std::size_t page) -> get_routes_response { auto req = bhttp::request{ bhttp::verb::get, std::format("/api/v1/routes.json?page_size=200?page={}", page), 11, // HTTP 1.1 }; req.set(bhttp::field::host, host); req.set("x-rwgps-api-key", api_key_); req.set("x-rwgps-auth-token", auth_token_); auto rsp = hc_.do_request(req); auto p = json::stream_parser{}; for (auto const frag : rsp.body().cdata()) { p.write(static_cast(frag.data()), frag.size()); } assert(p.done()); return json_value_to_get_routes_response(p.release()); } public: explicit client(net::io_context& ioc, log::logger const& l, std::string api_key, std::string auth_token) : l_{l.sub("rwgps-client")}, hc_{ioc}, api_key_{std::move(api_key)}, auth_token_{std::move(auth_token)} {} auto get_all_routes() -> std::vector { // TODO: make sure that there are no duplicates here. // What does RWGPS sort on, by default? // Consider using an associative container instead of a vector. auto record_count = 0uz; auto current_page = 0uz; auto routes = std::vector{}; while (true) { auto rsp = get_routes_page(current_page); if (rsp.meta.pagination.next_page_url) l_.debug("Next page URL: {}", *rsp.meta.pagination.next_page_url); routes.append_range(rsp.routes); if (rsp.meta.pagination.record_count > 0) record_count = rsp.meta.pagination.record_count; if (rsp.routes.empty() || routes.size() >= record_count) { break; } } return routes; } }; } // namespace routemon::rwgps