yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
project_plan.cc
Go to the documentation of this file.
1#include "core/project_plan.h"
2
3#include <algorithm>
4#include <cstdlib>
5#include <filesystem>
6#include <fstream>
7#include <initializer_list>
8#include <sstream>
9#include <system_error>
10#include <utility>
11
12#include "absl/strings/str_format.h"
13
14namespace yaze::core {
15namespace {
16
17class Reader {
18 public:
19 explicit Reader(std::string context) : context_(std::move(context)) {}
20
21 absl::Status status() const { return status_; }
22
23 void String(const PlanJson& obj, const char* key, std::string* out) {
24 auto it = obj.find(key);
25 if (it == obj.end() || it->is_null()) {
26 return;
27 }
28 if (!it->is_string()) {
29 Fail(key, "a string");
30 return;
31 }
32 *out = it->get<std::string>();
33 }
34
35 void Bool(const PlanJson& obj, const char* key, bool* out) {
36 auto it = obj.find(key);
37 if (it == obj.end() || it->is_null()) {
38 return;
39 }
40 if (!it->is_boolean()) {
41 Fail(key, "true or false");
42 return;
43 }
44 *out = it->get<bool>();
45 }
46
47 void Int(const PlanJson& obj, const char* key, int* out) {
48 auto it = obj.find(key);
49 if (it == obj.end() || it->is_null()) {
50 return;
51 }
52 if (!it->is_number_integer()) {
53 Fail(key, "an integer");
54 return;
55 }
56 *out = it->get<int>();
57 }
58
59 void Strings(const PlanJson& obj, const char* key,
60 std::vector<std::string>* out) {
61 auto it = obj.find(key);
62 if (it == obj.end() || it->is_null()) {
63 return;
64 }
65 if (!it->is_array()) {
66 Fail(key, "an array of strings");
67 return;
68 }
69 for (const auto& value : *it) {
70 if (!value.is_string()) {
71 Fail(key, "an array of strings");
72 return;
73 }
74 out->push_back(value.get<std::string>());
75 }
76 }
77
78 // Returns the array at `key`, or an empty array when absent.
79 const PlanJson& Array(const PlanJson& obj, const char* key) {
80 static const PlanJson kEmpty = PlanJson::array();
81 auto it = obj.find(key);
82 if (it == obj.end() || it->is_null()) {
83 return kEmpty;
84 }
85 if (!it->is_array()) {
86 Fail(key, "an array");
87 return kEmpty;
88 }
89 return *it;
90 }
91
92 bool RequireObject(const PlanJson& value, const std::string& where) {
93 if (value.is_object()) {
94 return true;
95 }
96 if (status_.ok()) {
97 status_ = absl::InvalidArgumentError(
98 absl::StrFormat("%s: %s must be an object", context_, where));
99 }
100 return false;
101 }
102
103 private:
104 void Fail(const char* key, const char* expected) {
105 if (status_.ok()) {
106 status_ = absl::InvalidArgumentError(
107 absl::StrFormat("%s: \"%s\" must be %s", context_, key, expected));
108 }
109 }
110
111 std::string context_;
112 absl::Status status_;
113};
114
115PlanJson Extras(const PlanJson& obj, std::initializer_list<const char*> known) {
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; });
121 if (!is_known) {
122 extra[it.key()] = it.value();
123 }
124 }
125 return extra;
126}
127
128void PutString(PlanJson& obj, const char* key, const std::string& value) {
129 if (!value.empty()) {
130 obj[key] = value;
131 }
132}
133
134void PutStrings(PlanJson& obj, const char* key,
135 const std::vector<std::string>& values) {
136 if (!values.empty()) {
137 obj[key] = values;
138 }
139}
140
141void PutExtras(PlanJson& obj, const PlanJson& extra) {
142 if (!extra.is_object()) {
143 return;
144 }
145 for (auto it = extra.begin(); it != extra.end(); ++it) {
146 if (!obj.contains(it.key())) {
147 obj[it.key()] = it.value();
148 }
149 }
150}
151
152absl::Status ReadGoal(const PlanJson& root, PlanGoal* goal) {
153 auto it = root.find("goal");
154 if (it == root.end() || it->is_null()) {
155 return absl::OkStatus();
156 }
157 Reader reader("goal");
158 if (!reader.RequireObject(*it, "goal")) {
159 return reader.status();
160 }
161 reader.String(*it, "title", &goal->title);
162 reader.String(*it, "detail", &goal->detail);
163 reader.String(*it, "due", &goal->due);
164 goal->extra = Extras(*it, {"title", "detail", "due"});
165 return reader.status();
166}
167
168absl::Status ReadMilestones(const PlanJson& root,
169 std::vector<PlanMilestone>* out) {
170 Reader top("plan");
171 const PlanJson& list = top.Array(root, "milestones");
172 for (size_t i = 0; i < list.size() && top.status().ok(); ++i) {
173 Reader reader(absl::StrFormat("milestones[%d]", i));
174 if (!reader.RequireObject(list[i], "entry")) {
175 return reader.status();
176 }
178 reader.String(list[i], "id", &m.id);
179 reader.String(list[i], "title", &m.title);
180 reader.String(list[i], "start", &m.start);
181 reader.String(list[i], "end", &m.end);
182 reader.String(list[i], "status", &m.status);
183 reader.String(list[i], "detail", &m.detail);
184 m.extra =
185 Extras(list[i], {"id", "title", "start", "end", "status", "detail"});
186 if (!reader.status().ok()) {
187 return reader.status();
188 }
189 out->push_back(std::move(m));
190 }
191 return top.status();
192}
193
194absl::Status ReadNeeds(const PlanJson& item, const std::string& context,
195 std::vector<PlanNeed>* out) {
196 Reader top(context);
197 const PlanJson& list = top.Array(item, "needs");
198 for (size_t i = 0; i < list.size() && top.status().ok(); ++i) {
199 Reader reader(absl::StrFormat("%s.needs[%d]", context, i));
200 if (!reader.RequireObject(list[i], "entry")) {
201 return reader.status();
202 }
203 PlanNeed need;
204 reader.String(list[i], "step", &need.step);
205 reader.String(list[i], "what", &need.what);
206 need.extra = Extras(list[i], {"step", "what"});
207 if (!reader.status().ok()) {
208 return reader.status();
209 }
210 out->push_back(std::move(need));
211 }
212 return top.status();
213}
214
215absl::Status ReadRoute(const PlanJson& root, ProjectPlan* plan) {
216 auto it = root.find("route");
217 if (it == root.end() || it->is_null()) {
218 return absl::OkStatus();
219 }
220 Reader top("route");
221 if (!top.RequireObject(*it, "route")) {
222 return top.status();
223 }
224 const PlanJson& route = *it;
225 plan->route_extra = Extras(route, {"steps", "items"});
226
227 const PlanJson& steps = top.Array(route, "steps");
228 for (size_t i = 0; i < steps.size() && top.status().ok(); ++i) {
229 Reader reader(absl::StrFormat("route.steps[%d]", i));
230 if (!reader.RequireObject(steps[i], "entry")) {
231 return reader.status();
232 }
233 PlanRouteStep step;
234 reader.String(steps[i], "id", &step.id);
235 reader.String(steps[i], "code", &step.code);
236 reader.String(steps[i], "title", &step.title);
237 reader.String(steps[i], "note", &step.note);
238 reader.String(steps[i], "group", &step.group);
239 reader.String(steps[i], "kind", &step.kind);
240 reader.Strings(steps[i], "targets", &step.targets);
241 step.extra = Extras(
242 steps[i], {"id", "code", "title", "note", "group", "kind", "targets"});
243 if (!reader.status().ok()) {
244 return reader.status();
245 }
246 plan->route_steps.push_back(std::move(step));
247 }
248
249 const PlanJson& items = top.Array(route, "items");
250 for (size_t i = 0; i < items.size() && top.status().ok(); ++i) {
251 const std::string context = absl::StrFormat("route.items[%d]", i);
252 Reader reader(context);
253 if (!reader.RequireObject(items[i], "entry")) {
254 return reader.status();
255 }
256 PlanRouteItem item;
257 reader.String(items[i], "id", &item.id);
258 reader.String(items[i], "name", &item.name);
259 reader.String(items[i], "step", &item.step);
260 reader.String(items[i], "category", &item.category);
261 reader.Bool(items[i], "required", &item.required);
262 reader.String(items[i], "status", &item.status);
263 reader.String(items[i], "where", &item.where);
264 reader.String(items[i], "note", &item.note);
265 item.extra = Extras(items[i], {"id", "name", "step", "category", "required",
266 "status", "where", "note", "needs"});
267 if (!reader.status().ok()) {
268 return reader.status();
269 }
270 auto needs_status = ReadNeeds(items[i], context, &item.needs);
271 if (!needs_status.ok()) {
272 return needs_status;
273 }
274 plan->route_items.push_back(std::move(item));
275 }
276 return top.status();
277}
278
279absl::Status ReadDecisions(const PlanJson& root,
280 std::vector<PlanDecision>* out) {
281 Reader top("plan");
282 const PlanJson& list = top.Array(root, "decisions");
283 for (size_t i = 0; i < list.size() && top.status().ok(); ++i) {
284 Reader reader(absl::StrFormat("decisions[%d]", i));
285 if (!reader.RequireObject(list[i], "entry")) {
286 return reader.status();
287 }
288 PlanDecision d;
289 reader.String(list[i], "id", &d.id);
290 reader.String(list[i], "title", &d.title);
291 reader.String(list[i], "status", &d.status);
292 reader.Int(list[i], "priority", &d.priority);
293 reader.String(list[i], "recommendation", &d.recommendation);
294 reader.String(list[i], "answer", &d.answer);
295 reader.Strings(list[i], "targets", &d.targets);
296 reader.Strings(list[i], "refs", &d.refs);
297 d.extra = Extras(list[i], {"id", "title", "status", "priority",
298 "recommendation", "answer", "targets", "refs"});
299 if (!reader.status().ok()) {
300 return reader.status();
301 }
302 out->push_back(std::move(d));
303 }
304 return top.status();
305}
306
307absl::Status ReadReferences(const PlanJson& root,
308 std::vector<PlanReference>* out) {
309 Reader top("plan");
310 const PlanJson& list = top.Array(root, "references");
311 for (size_t i = 0; i < list.size() && top.status().ok(); ++i) {
312 Reader reader(absl::StrFormat("references[%d]", i));
313 if (!reader.RequireObject(list[i], "entry")) {
314 return reader.status();
315 }
317 reader.String(list[i], "label", &r.label);
318 reader.String(list[i], "path", &r.path);
319 reader.String(list[i], "author", &r.author);
320 reader.String(list[i], "date", &r.date);
321 r.extra = Extras(list[i], {"label", "path", "author", "date"});
322 if (!reader.status().ok()) {
323 return reader.status();
324 }
325 out->push_back(std::move(r));
326 }
327 return top.status();
328}
329
330} // namespace
331
332PlanTarget PlanTarget::Parse(std::string_view text) {
333 PlanTarget target;
334 const size_t colon = text.find(':');
335 if (colon == std::string_view::npos) {
336 target.id = std::string(text);
337 return target;
338 }
339 target.kind = std::string(text.substr(0, colon));
340 target.id = std::string(text.substr(colon + 1));
341 return target;
342}
343
345 if ((kind != "room" && kind != "map") || id.empty()) {
346 return -1;
347 }
348 char* end = nullptr;
349 const long value = std::strtol(id.c_str(), &end, 0);
350 if (end == id.c_str() || *end != '\0' || value < 0) {
351 return -1;
352 }
353 return static_cast<int>(value);
354}
355
357 ProjectPlan plan;
358 plan.title = std::move(title);
359 return plan;
360}
361
362absl::StatusOr<ProjectPlan> ProjectPlan::Parse(std::string_view text) {
363 PlanJson root = PlanJson::parse(text.begin(), text.end(), nullptr,
364 /*allow_exceptions=*/false);
365 if (root.is_discarded()) {
366 return absl::InvalidArgumentError("Plan file is not valid JSON");
367 }
368 if (!root.is_object()) {
369 return absl::InvalidArgumentError("Plan file must be a JSON object");
370 }
371 auto schema = root.find("schema");
372 if (schema == root.end() || !schema->is_string() ||
373 schema->get<std::string>() != kProjectPlanSchema) {
374 return absl::InvalidArgumentError(
375 absl::StrFormat("Plan file must declare \"schema\": \"%s\"",
376 std::string(kProjectPlanSchema)));
377 }
378
379 ProjectPlan plan;
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();
385 }
386 for (auto status :
387 {ReadGoal(root, &plan.goal), ReadMilestones(root, &plan.milestones),
388 ReadRoute(root, &plan), ReadDecisions(root, &plan.decisions),
389 ReadReferences(root, &plan.references)}) {
390 if (!status.ok()) {
391 return status;
392 }
393 }
394 plan.extra = Extras(root, {"schema", "title", "about", "goal", "milestones",
395 "route", "decisions", "references"});
396 return plan;
397}
398
399absl::StatusOr<ProjectPlan> ProjectPlan::LoadFromFile(const std::string& path) {
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));
404 }
405 std::stringstream buffer;
406 buffer << file.rdbuf();
407 auto plan = Parse(buffer.str());
408 if (!plan.ok()) {
409 return absl::Status(
410 plan.status().code(),
411 absl::StrFormat("%s: %s", path, plan.status().message()));
412 }
413 return plan;
414}
415
416std::string ProjectPlan::Serialize() const {
417 PlanJson root = PlanJson::object();
418 root["schema"] = std::string(kProjectPlanSchema);
419 PutString(root, "title", title);
420 PutString(root, "about", about);
421
422 PlanJson goal_json = PlanJson::object();
423 PutString(goal_json, "title", goal.title);
424 PutString(goal_json, "detail", goal.detail);
425 PutString(goal_json, "due", goal.due);
426 PutExtras(goal_json, goal.extra);
427 if (!goal_json.empty()) {
428 root["goal"] = std::move(goal_json);
429 }
430
431 PlanJson milestones_json = PlanJson::array();
432 for (const auto& m : milestones) {
433 PlanJson j = PlanJson::object();
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));
442 }
443 root["milestones"] = std::move(milestones_json);
444
445 PlanJson route = PlanJson::object();
446 PlanJson steps = PlanJson::array();
447 for (const auto& s : route_steps) {
448 PlanJson j = 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));
458 }
459 route["steps"] = std::move(steps);
460 PlanJson items = PlanJson::array();
461 for (const auto& item : route_items) {
462 PlanJson j = PlanJson::object();
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()) {
472 PlanJson needs = PlanJson::array();
473 for (const auto& need : item.needs) {
474 PlanJson n = PlanJson::object();
475 PutString(n, "step", need.step);
476 PutString(n, "what", need.what);
477 PutExtras(n, need.extra);
478 needs.push_back(std::move(n));
479 }
480 j["needs"] = std::move(needs);
481 }
482 PutExtras(j, item.extra);
483 items.push_back(std::move(j));
484 }
485 route["items"] = std::move(items);
486 PutExtras(route, route_extra);
487 root["route"] = std::move(route);
488
489 PlanJson decisions_json = PlanJson::array();
490 for (const auto& d : decisions) {
491 PlanJson j = PlanJson::object();
492 PutString(j, "id", d.id);
493 if (d.priority != 0) {
494 j["priority"] = d.priority;
495 }
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));
504 }
505 root["decisions"] = std::move(decisions_json);
506
507 PlanJson references_json = PlanJson::array();
508 for (const auto& r : references) {
509 PlanJson j = PlanJson::object();
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));
516 }
517 root["references"] = std::move(references_json);
518
519 PutExtras(root, extra);
520 return root.dump(1) + "\n";
521}
522
523absl::Status ProjectPlan::SaveToFile(const std::string& path) const {
524 const std::filesystem::path target(path);
525 std::filesystem::path temp = target;
526 temp += ".tmp";
527 {
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()));
532 }
533 const std::string text = Serialize();
534 file.write(text.data(), static_cast<std::streamsize>(text.size()));
535 file.close();
536 if (file.fail()) {
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()));
541 }
542 }
543 std::error_code error;
544 std::filesystem::rename(temp, target, error);
545 if (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()));
550 }
551 return absl::OkStatus();
552}
553
554int ProjectPlan::FindStep(std::string_view id) const {
555 for (size_t i = 0; i < route_steps.size(); ++i) {
556 if (route_steps[i].id == id) {
557 return static_cast<int>(i);
558 }
559 }
560 return -1;
561}
562
563std::vector<const PlanRouteItem*> ProjectPlan::ItemsAtStep(
564 std::string_view step) const {
565 std::vector<const PlanRouteItem*> out;
566 for (const auto& item : route_items) {
567 if (item.step == step) {
568 out.push_back(&item);
569 }
570 }
571 return out;
572}
573
574std::vector<const PlanDecision*> ProjectPlan::OpenDecisions() const {
575 std::vector<const PlanDecision*> out;
576 for (const auto& d : decisions) {
577 if (d.status.empty() || d.status == "open") {
578 out.push_back(&d);
579 }
580 }
581 std::stable_sort(out.begin(), out.end(),
582 [](const PlanDecision* a, const PlanDecision* b) {
583 const int pa = a->priority > 0 ? a->priority : 1 << 30;
584 const int pb = b->priority > 0 ? b->priority : 1 << 30;
585 return pa < pb;
586 });
587 return out;
588}
589
591 std::string_view today) const {
592 for (const auto& m : milestones) {
593 if (m.status == "active") {
594 return &m;
595 }
596 }
597 for (const auto& m : milestones) {
598 if (!m.start.empty() && !m.end.empty() && m.start <= today &&
599 today <= m.end) {
600 return &m;
601 }
602 }
603 return nullptr;
604}
605
606} // namespace yaze::core
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)
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)
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::vector< PlanNeed > needs
std::vector< std::string > targets
static PlanTarget Parse(std::string_view text)