Adds api_client support for GET /licenses/{key}/schedule with If-None-Match
and target-payload parsing (version/type/daily/weekly + legacy blocks), an
atomic on-disk schedule cache, a pure schedule engine (half-open blocks,
midnight self-wrap for daily, Sunday->Monday spillover for weekly, invalid
blocks skipped with a warning), and a background ScheduleManager that loads
the cache offline and pulls on boot and every 5 minutes. Adds engine/cache
tests against the shared docs/fixtures, including 304, corrupt cache, and
offline cases.
113 lines
3.6 KiB
C++
113 lines
3.6 KiB
C++
#include "schedule/schedule_engine.h"
|
|
|
|
namespace radiantwave::schedule {
|
|
|
|
namespace {
|
|
|
|
const char* dayKey(Weekday day) {
|
|
switch (day) {
|
|
case Weekday::Mon: return "mon";
|
|
case Weekday::Tue: return "tue";
|
|
case Weekday::Wed: return "wed";
|
|
case Weekday::Thu: return "thu";
|
|
case Weekday::Fri: return "fri";
|
|
case Weekday::Sat: return "sat";
|
|
case Weekday::Sun: return "sun";
|
|
}
|
|
return "mon";
|
|
}
|
|
|
|
bool blockIsValid(const api::TargetBlock& block) {
|
|
return !block.proclamation_ids.empty() &&
|
|
block.start_minutes >= 0 && block.start_minutes <= 1440 &&
|
|
block.end_minutes >= 0 && block.end_minutes <= 1440 &&
|
|
block.start_minutes != block.end_minutes;
|
|
}
|
|
|
|
void warnInvalid(const api::TargetBlock& block,
|
|
const std::function<void(const std::string&)>& warn) {
|
|
if (warn) {
|
|
warn("Skipping invalid schedule block id=" + block.id);
|
|
}
|
|
}
|
|
|
|
// Find a matching block among `candidates` at `minutes`, skipping invalid ones.
|
|
bool match(const std::vector<api::TargetBlock>& candidates, int minutes,
|
|
api::TargetBlock& out,
|
|
const std::function<void(const std::string&)>& warn) {
|
|
for (const auto& block : candidates) {
|
|
if (!blockIsValid(block)) {
|
|
warnInvalid(block, warn);
|
|
continue;
|
|
}
|
|
if (minuteInBlock(minutes, block.start_minutes, block.end_minutes)) {
|
|
out = block;
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
bool minuteInBlock(int minutes, int startMinutes, int endMinutes) {
|
|
if (startMinutes < endMinutes) {
|
|
return minutes >= startMinutes && minutes < endMinutes;
|
|
}
|
|
// Midnight-crossing: [start, 1440) plus [0, end).
|
|
return minutes >= startMinutes || minutes < endMinutes;
|
|
}
|
|
|
|
Weekday weekdayFromIndex(int index) {
|
|
if (index < 0) index = 0;
|
|
return static_cast<Weekday>(index % 7);
|
|
}
|
|
|
|
Decision evaluate(const api::SchedulePayload& payload, int minutes, Weekday weekday,
|
|
std::function<void(const std::string&)> warn) {
|
|
Decision decision;
|
|
if (minutes < 0) minutes = 0;
|
|
minutes %= 1440;
|
|
|
|
if (payload.type == "daily") {
|
|
decision.hasBlock = match(payload.daily, minutes, decision.block, warn);
|
|
return decision;
|
|
}
|
|
|
|
if (payload.type == "weekly") {
|
|
// Only blocks for today...
|
|
std::vector<api::TargetBlock> today;
|
|
const std::string todayKey = dayKey(weekday);
|
|
for (const auto& block : payload.weekly) {
|
|
if (block.day == todayKey) today.push_back(block);
|
|
}
|
|
if (match(today, minutes, decision.block, warn)) {
|
|
decision.hasBlock = true;
|
|
return decision;
|
|
}
|
|
|
|
// ...or a crossing Block from yesterday that spills into today's early
|
|
// hours. (Sunday's spill reaches Monday via the day rollover.)
|
|
const int prevIndex = (static_cast<int>(weekday) + 6) % 7;
|
|
const std::string prevKey = dayKey(static_cast<Weekday>(prevIndex));
|
|
for (const auto& block : payload.weekly) {
|
|
if (block.day != prevKey) continue;
|
|
if (block.end_minutes >= block.start_minutes) continue; // not crossing
|
|
if (!blockIsValid(block)) {
|
|
warnInvalid(block, warn);
|
|
continue;
|
|
}
|
|
if (minutes < block.end_minutes) {
|
|
decision.block = block;
|
|
decision.hasBlock = true;
|
|
return decision;
|
|
}
|
|
}
|
|
return decision;
|
|
}
|
|
|
|
return decision; // type null / unknown -> no active block
|
|
}
|
|
|
|
} // namespace radiantwave::schedule
|