#pragma once #include #include #include #include "json_scan.h" namespace roro::release { // What the device needs to know about one Gitea release (docs/milestones/R1.md, #6). struct Release { std::string tag; // "v0.10.0" std::string published; // "2026-10-06T08:11:31Z" std::string notes; // the first paragraph of the text: the tag's message std::string otaUrl; // where the signed Update File is uint32_t otaSize = 0; bool draft = false, prerelease = false; // Something that can be installed and that the project meant to publish (drafts and // pre-releases are left out for now: a release channel is #53). bool usable() const { return !draft && !prerelease && !tag.empty() && !otaUrl.empty(); } std::string date() const { return published.substr(0, 10); } // "2026-10-06" }; // Reads Gitea's answer as it arrives: `releases/latest` (one object) or `releases?limit=N` (a list). class ReleaseReader { public: explicit ReleaseReader(size_t maxReleases = 10); void feed(const char* data, size_t len) { scanner_.feed(data, len); } void end(); // after the last chunk bool ok() const { return !scanner_.failed(); } bool complete() const { return scanner_.done(); } const std::vector& releases() const { return releases_; } private: void value(const std::string& path, const std::string& text, bool isString, bool truncated); void flushAsset(); JsonScanner scanner_; size_t max_; std::vector releases_; int assetRelease_ = -1, assetIndex_ = -1; std::string assetName_, assetUrl_; uint32_t assetSize_ = 0; }; // The first paragraph of a release's text, trimmed; "..." if the text was cut before it ended. std::string firstParagraph(const std::string& text, bool truncated); } // namespace roro::release