7#include <initializer_list>
12#include "absl/strings/str_format.h"
19 explicit Reader(std::string context) : context_(std::move(context)) {}
21 absl::Status
status()
const {
return status_; }
24 auto it = obj.find(key);
25 if (it == obj.end() || it->is_null()) {
28 if (!it->is_string()) {
29 Fail(key,
"a string");
32 *out = it->get<std::string>();
36 auto it = obj.find(key);
37 if (it == obj.end() || it->is_null()) {
40 if (!it->is_boolean()) {
41 Fail(key,
"true or false");
44 *out = it->get<
bool>();
48 auto it = obj.find(key);
49 if (it == obj.end() || it->is_null()) {
52 if (!it->is_number_integer()) {
53 Fail(key,
"an integer");
56 *out = it->get<
int>();
60 std::vector<std::string>* out) {
61 auto it = obj.find(key);
62 if (it == obj.end() || it->is_null()) {
65 if (!it->is_array()) {
66 Fail(key,
"an array of strings");
69 for (
const auto& value : *it) {
70 if (!value.is_string()) {
71 Fail(key,
"an array of strings");
74 out->push_back(value.get<std::string>());
80 static const PlanJson kEmpty = PlanJson::array();
81 auto it = obj.find(key);
82 if (it == obj.end() || it->is_null()) {
85 if (!it->is_array()) {
86 Fail(key,
"an array");
93 if (value.is_object()) {
97 status_ = absl::InvalidArgumentError(
98 absl::StrFormat(
"%s: %s must be an object", context_, where));
104 void Fail(
const char* key,
const char* expected) {
106 status_ = absl::InvalidArgumentError(
107 absl::StrFormat(
"%s: \"%s\" must be %s", context_, key, expected));
116 PlanJson extra = PlanJson::object();
117 for (
auto it = obj.begin(); it != obj.end(); ++it) {
118 const bool is_known =
119 std::any_of(known.begin(), known.end(),
120 [&](
const char* key) { return it.key() == key; });
122 extra[it.key()] = it.value();
129 if (!value.empty()) {
135 const std::vector<std::string>& values) {
136 if (!values.empty()) {
142 if (!extra.is_object()) {
145 for (
auto it = extra.begin(); it != extra.end(); ++it) {
146 if (!obj.contains(it.key())) {
147 obj[it.key()] = it.value();
153 auto it = root.find(
"goal");
154 if (it == root.end() || it->is_null()) {
155 return absl::OkStatus();
164 goal->
extra =
Extras(*it, {
"title",
"detail",
"due"});
169 std::vector<PlanMilestone>* out) {
172 for (
size_t i = 0; i < list.size() && top.
status().ok(); ++i) {
173 Reader reader(absl::StrFormat(
"milestones[%d]", i));
178 reader.
String(list[i],
"id", &m.
id);
185 Extras(list[i], {
"id",
"title",
"start",
"end",
"status",
"detail"});
186 if (!reader.
status().ok()) {
189 out->push_back(std::move(m));
195 std::vector<PlanNeed>* out) {
198 for (
size_t i = 0; i < list.size() && top.
status().ok(); ++i) {
199 Reader reader(absl::StrFormat(
"%s.needs[%d]", context, i));
207 if (!reader.
status().ok()) {
210 out->push_back(std::move(need));
216 auto it = root.find(
"route");
217 if (it == root.end() || it->is_null()) {
218 return absl::OkStatus();
228 for (
size_t i = 0; i < steps.size() && top.
status().ok(); ++i) {
229 Reader reader(absl::StrFormat(
"route.steps[%d]", i));
234 reader.
String(steps[i],
"id", &step.
id);
242 steps[i], {
"id",
"code",
"title",
"note",
"group",
"kind",
"targets"});
243 if (!reader.
status().ok()) {
250 for (
size_t i = 0; i < items.size() && top.
status().ok(); ++i) {
251 const std::string context = absl::StrFormat(
"route.items[%d]", i);
257 reader.
String(items[i],
"id", &item.
id);
265 item.
extra =
Extras(items[i], {
"id",
"name",
"step",
"category",
"required",
266 "status",
"where",
"note",
"needs"});
267 if (!reader.
status().ok()) {
271 if (!needs_status.ok()) {
280 std::vector<PlanDecision>* out) {
283 for (
size_t i = 0; i < list.size() && top.
status().ok(); ++i) {
284 Reader reader(absl::StrFormat(
"decisions[%d]", i));
289 reader.
String(list[i],
"id", &d.
id);
297 d.
extra =
Extras(list[i], {
"id",
"title",
"status",
"priority",
298 "recommendation",
"answer",
"targets",
"refs"});
299 if (!reader.
status().ok()) {
302 out->push_back(std::move(d));
308 std::vector<PlanReference>* out) {
311 for (
size_t i = 0; i < list.size() && top.
status().ok(); ++i) {
312 Reader reader(absl::StrFormat(
"references[%d]", i));
321 r.
extra =
Extras(list[i], {
"label",
"path",
"author",
"date"});
322 if (!reader.
status().ok()) {
325 out->push_back(std::move(r));
334 const size_t colon = text.find(
':');
335 if (colon == std::string_view::npos) {
336 target.
id = std::string(text);
339 target.
kind = std::string(text.substr(0, colon));
340 target.
id = std::string(text.substr(colon + 1));
345 if ((
kind !=
"room" &&
kind !=
"map") ||
id.empty()) {
349 const long value = std::strtol(
id.c_str(), &end, 0);
350 if (end ==
id.c_str() || *end !=
'\0' || value < 0) {
353 return static_cast<int>(value);
363 PlanJson root = PlanJson::parse(text.begin(), text.end(),
nullptr,
365 if (root.is_discarded()) {
366 return absl::InvalidArgumentError(
"Plan file is not valid JSON");
368 if (!root.is_object()) {
369 return absl::InvalidArgumentError(
"Plan file must be a JSON object");
371 auto schema = root.find(
"schema");
372 if (schema == root.end() || !schema->is_string() ||
374 return absl::InvalidArgumentError(
375 absl::StrFormat(
"Plan file must declare \"schema\": \"%s\"",
380 Reader reader(
"plan");
381 reader.String(root,
"title", &plan.
title);
382 reader.String(root,
"about", &plan.
about);
383 if (!reader.status().ok()) {
384 return reader.status();
387 {ReadGoal(root, &plan.
goal), ReadMilestones(root, &plan.
milestones),
388 ReadRoute(root, &plan), ReadDecisions(root, &plan.
decisions),
394 plan.
extra = Extras(root, {
"schema",
"title",
"about",
"goal",
"milestones",
395 "route",
"decisions",
"references"});
400 std::ifstream file(path, std::ios::binary);
401 if (!file.is_open()) {
402 return absl::NotFoundError(
403 absl::StrFormat(
"Cannot open plan file: %s", path));
405 std::stringstream buffer;
406 buffer << file.rdbuf();
407 auto plan =
Parse(buffer.str());
410 plan.status().code(),
411 absl::StrFormat(
"%s: %s", path, plan.status().message()));
419 PutString(root,
"title",
title);
420 PutString(root,
"about",
about);
422 PlanJson goal_json = PlanJson::object();
423 PutString(goal_json,
"title",
goal.
title);
425 PutString(goal_json,
"due",
goal.
due);
427 if (!goal_json.empty()) {
428 root[
"goal"] = std::move(goal_json);
431 PlanJson milestones_json = PlanJson::array();
434 PutString(j,
"id", m.id);
435 PutString(j,
"title", m.title);
436 PutString(j,
"start", m.start);
437 PutString(j,
"end", m.end);
438 PutString(j,
"status", m.status);
439 PutString(j,
"detail", m.detail);
440 PutExtras(j, m.extra);
441 milestones_json.push_back(std::move(j));
443 root[
"milestones"] = std::move(milestones_json);
445 PlanJson route = PlanJson::object();
449 PutString(j,
"id", s.id);
450 PutString(j,
"code", s.code);
451 PutString(j,
"title", s.title);
452 PutString(j,
"note", s.note);
453 PutString(j,
"group", s.group);
454 PutString(j,
"kind", s.kind);
455 PutStrings(j,
"targets", s.targets);
456 PutExtras(j, s.extra);
457 steps.push_back(std::move(j));
459 route[
"steps"] = std::move(steps);
463 PutString(j,
"id", item.id);
464 PutString(j,
"name", item.name);
465 PutString(j,
"step", item.step);
466 PutString(j,
"category", item.category);
467 j[
"required"] = item.required;
468 PutString(j,
"status", item.status);
469 PutString(j,
"where", item.where);
470 PutString(j,
"note", item.note);
471 if (!item.needs.empty()) {
473 for (
const auto& need : item.needs) {
475 PutString(n,
"step", need.step);
476 PutString(n,
"what", need.what);
477 PutExtras(n, need.extra);
478 needs.push_back(std::move(n));
480 j[
"needs"] = std::move(needs);
482 PutExtras(j, item.extra);
483 items.push_back(std::move(j));
485 route[
"items"] = std::move(items);
487 root[
"route"] = std::move(route);
489 PlanJson decisions_json = PlanJson::array();
492 PutString(j,
"id", d.id);
493 if (d.priority != 0) {
494 j[
"priority"] = d.priority;
496 PutString(j,
"status", d.status);
497 PutString(j,
"title", d.title);
498 PutString(j,
"recommendation", d.recommendation);
499 PutString(j,
"answer", d.answer);
500 PutStrings(j,
"targets", d.targets);
501 PutStrings(j,
"refs", d.refs);
502 PutExtras(j, d.extra);
503 decisions_json.push_back(std::move(j));
505 root[
"decisions"] = std::move(decisions_json);
507 PlanJson references_json = PlanJson::array();
510 PutString(j,
"label", r.label);
511 PutString(j,
"path", r.path);
512 PutString(j,
"author", r.author);
513 PutString(j,
"date", r.date);
514 PutExtras(j, r.extra);
515 references_json.push_back(std::move(j));
517 root[
"references"] = std::move(references_json);
519 PutExtras(root,
extra);
520 return root.dump(1) +
"\n";
524 const std::filesystem::path target(path);
525 std::filesystem::path temp = target;
528 std::ofstream file(temp, std::ios::binary | std::ios::trunc);
529 if (!file.is_open()) {
530 return absl::InvalidArgumentError(
531 absl::StrFormat(
"Cannot write plan file: %s", temp.string()));
534 file.write(text.data(),
static_cast<std::streamsize
>(text.size()));
537 std::error_code ignored;
538 std::filesystem::remove(temp, ignored);
539 return absl::InternalError(
540 absl::StrFormat(
"Failed to write plan file: %s", temp.string()));
543 std::error_code error;
544 std::filesystem::rename(temp, target, error);
546 std::error_code ignored;
547 std::filesystem::remove(temp, ignored);
548 return absl::InternalError(absl::StrFormat(
549 "Failed to replace plan file %s: %s", path, error.message()));
551 return absl::OkStatus();
557 return static_cast<int>(i);
564 std::string_view step)
const {
565 std::vector<const PlanRouteItem*> out;
567 if (item.step == step) {
568 out.push_back(&item);
575 std::vector<const PlanDecision*> out;
577 if (d.status.empty() || d.status ==
"open") {
581 std::stable_sort(out.begin(), out.end(),
583 const int pa = a->priority > 0 ? a->priority : 1 << 30;
584 const int pb = b->priority > 0 ? b->priority : 1 << 30;
591 std::string_view today)
const {
593 if (m.status ==
"active") {
598 if (!m.start.empty() && !m.end.empty() && m.start <= today &&
std::string Serialize() const
absl::Status SaveToFile(const std::string &path) const
static ProjectPlan CreateEmpty(std::string title)
static absl::StatusOr< ProjectPlan > Parse(std::string_view text)
std::vector< PlanReference > references
std::vector< const PlanRouteItem * > ItemsAtStep(std::string_view step) const
const PlanMilestone * CurrentMilestone(std::string_view today) const
std::vector< const PlanDecision * > OpenDecisions() const
std::vector< PlanMilestone > milestones
static absl::StatusOr< ProjectPlan > LoadFromFile(const std::string &path)
std::vector< PlanRouteItem > route_items
int FindStep(std::string_view id) const
std::vector< PlanRouteStep > route_steps
std::vector< PlanDecision > decisions
const PlanJson & Array(const PlanJson &obj, const char *key)
void Int(const PlanJson &obj, const char *key, int *out)
Reader(std::string context)
void Strings(const PlanJson &obj, const char *key, std::vector< std::string > *out)
void Fail(const char *key, const char *expected)
bool RequireObject(const PlanJson &value, const std::string &where)
absl::Status status() const
void Bool(const PlanJson &obj, const char *key, bool *out)
void String(const PlanJson &obj, const char *key, std::string *out)
absl::Status ReadMilestones(const PlanJson &root, std::vector< PlanMilestone > *out)
void PutExtras(PlanJson &obj, const PlanJson &extra)
absl::Status ReadRoute(const PlanJson &root, ProjectPlan *plan)
absl::Status ReadReferences(const PlanJson &root, std::vector< PlanReference > *out)
void PutString(PlanJson &obj, const char *key, const std::string &value)
absl::Status ReadDecisions(const PlanJson &root, std::vector< PlanDecision > *out)
void PutStrings(PlanJson &obj, const char *key, const std::vector< std::string > &values)
absl::Status ReadGoal(const PlanJson &root, PlanGoal *goal)
PlanJson Extras(const PlanJson &obj, std::initializer_list< const char * > known)
absl::Status ReadNeeds(const PlanJson &item, const std::string &context, std::vector< PlanNeed > *out)
constexpr std::string_view kProjectPlanSchema
nlohmann::ordered_json PlanJson
std::vector< std::string > targets
std::vector< std::string > refs
std::string recommendation
std::vector< PlanNeed > needs
std::vector< std::string > targets
static PlanTarget Parse(std::string_view text)