Files
App/src/storage/state_repository.cpp
2026-06-13 10:49:41 +07:00

323 lines
9.5 KiB
C++

#include "storage/state_repository.hpp"
#include "domain/layout_profile.hpp"
#include "domain/layout_schema.hpp"
#include "util/file_util.hpp"
#include "util/id_util.hpp"
#include "util/string_util.hpp"
namespace lm {
std::filesystem::path StateRepository::modelsDir() const
{
return app_.data_path.parent_path() / "models";
}
std::filesystem::path StateRepository::profileFilePath(const std::string& id) const
{
return modelsDir() / (id + ".json");
}
std::optional<nlohmann::json> StateRepository::loadProfileFromDisk(const std::string& id) const
{
const auto raw = FileUtil::readBinary(profileFilePath(id));
if (raw.empty())
return std::nullopt;
try
{
return nlohmann::json::parse(raw);
}
catch (...)
{
return std::nullopt;
}
}
bool StateRepository::saveProfileToDisk(const nlohmann::json& profile) const
{
if (!profile.is_object() || !profile.contains("id") || !profile["id"].is_string())
return false;
std::error_code ec;
std::filesystem::create_directories(modelsDir(), ec);
auto body = profile.dump(2);
body.push_back('\n');
return FileUtil::writeBinaryAtomic(profileFilePath(profile["id"].get<std::string>()), body);
}
bool StateRepository::deleteProfileFile(const std::string& id) const
{
std::error_code ec;
std::filesystem::remove(profileFilePath(id), ec);
return true;
}
void StateRepository::loadActiveCache()
{
nlohmann::json& state = app_.state;
const auto idx = LayoutProfile::findActiveIndex(state);
if (!idx)
return;
const std::string id = state["layouts"][*idx]["id"].get<std::string>();
nlohmann::json profile;
if (auto loaded = loadProfileFromDisk(id))
profile = *loaded;
else
{
profile = LayoutProfile::make(state["layouts"][*idx]["name"].get<std::string>(),
LayoutSchema::defaultLayoutObject(),
nlohmann::json::array());
profile["id"] = id;
if (state["layouts"][*idx].contains("created_at"))
profile["created_at"] = state["layouts"][*idx]["created_at"];
}
if (!profile.contains("layout") || !profile["layout"].is_object())
profile["layout"] = LayoutSchema::defaultLayoutObject();
if (!profile.contains("lidars") || !profile["lidars"].is_array())
profile["lidars"] = nlohmann::json::array();
if (!profile.contains("imus") || !profile["imus"].is_array())
profile["imus"] = nlohmann::json::array();
LayoutSchema::ensure(profile["layout"]);
state["layout"] = profile["layout"];
state["lidars"] = profile["lidars"];
state["imus"] = profile["imus"];
}
bool StateRepository::persistActiveProfile()
{
nlohmann::json& state = app_.state;
const auto idx = LayoutProfile::findActiveIndex(state);
if (!idx)
return false;
auto& entry = state["layouts"][*idx];
nlohmann::json profile;
profile["id"] = entry["id"];
profile["name"] = entry.contains("name") ? entry["name"] : nlohmann::json("Layout");
profile["created_at"] = entry.value("created_at", IdUtil::nowIso8601());
profile["updated_at"] = IdUtil::nowIso8601();
profile["layout"] = state["layout"];
profile["lidars"] = state["lidars"];
profile["imus"] = state.contains("imus") && state["imus"].is_array() ? state["imus"] : nlohmann::json::array();
LayoutSchema::ensure(profile["layout"]);
if (!saveProfileToDisk(profile))
return false;
entry = LayoutProfile::catalogEntryFromProfile(profile);
return true;
}
nlohmann::json StateRepository::globalStateForDisk(const nlohmann::json& state) const
{
nlohmann::json out = nlohmann::json::object();
out["version"] = 3;
if (state.contains("active_layout_id"))
out["active_layout_id"] = state["active_layout_id"];
out["layouts"] = nlohmann::json::array();
if (state.contains("layouts") && state["layouts"].is_array())
{
for (const auto& entry : state["layouts"])
{
if (!entry.is_object() || !entry.contains("id") || !entry.contains("name"))
continue;
if (entry.contains("layout"))
out["layouts"].push_back(LayoutProfile::catalogEntryFromProfile(entry));
else
out["layouts"].push_back(entry);
}
}
return out;
}
void StateRepository::stripInlineProfiles(nlohmann::json& state) const
{
if (!state.contains("layouts") || !state["layouts"].is_array())
return;
nlohmann::json catalog = nlohmann::json::array();
for (const auto& entry : state["layouts"])
{
if (!entry.is_object() || !entry.contains("id"))
continue;
if (entry.contains("layout"))
catalog.push_back(LayoutProfile::catalogEntryFromProfile(entry));
else
catalog.push_back(entry);
}
state["layouts"] = catalog;
}
void StateRepository::migrateStorage()
{
nlohmann::json& s = app_.state;
if (!s.is_object())
s = nlohmann::json::object();
const int version = s.contains("version") && s["version"].is_number_integer() ? s["version"].get<int>() : 1;
if (!s.contains("layouts") || !s["layouts"].is_array() || s["layouts"].empty())
{
nlohmann::json layout = s.contains("layout") && s["layout"].is_object() ? s["layout"] : LayoutSchema::defaultLayoutObject();
nlohmann::json lidars = s.contains("lidars") && s["lidars"].is_array() ? s["lidars"] : nlohmann::json::array();
nlohmann::json profile = LayoutProfile::make("Mặc định", layout, lidars);
LayoutSchema::ensure(profile["layout"]);
saveProfileToDisk(profile);
s["layouts"] = nlohmann::json::array({LayoutProfile::catalogEntryFromProfile(profile)});
s["active_layout_id"] = profile["id"].get<std::string>();
}
else if (version < 3)
{
if (!s.contains("active_layout_id") || !s["active_layout_id"].is_string() ||
!LayoutProfile::findIndex(s, s["active_layout_id"].get<std::string>()))
{
s["active_layout_id"] = s["layouts"][0]["id"].get<std::string>();
}
nlohmann::json catalog = nlohmann::json::array();
for (auto& entry : s["layouts"])
{
if (!entry.is_object() || !entry.contains("id"))
continue;
nlohmann::json profile;
if (entry.contains("layout"))
{
profile = entry;
if (!profile.contains("name"))
profile["name"] = "Layout";
if (!profile.contains("lidars") || !profile["lidars"].is_array())
profile["lidars"] = nlohmann::json::array();
if (!profile.contains("created_at"))
profile["created_at"] = IdUtil::nowIso8601();
LayoutProfile::touch(profile);
LayoutSchema::ensure(profile["layout"]);
saveProfileToDisk(profile);
catalog.push_back(LayoutProfile::catalogEntryFromProfile(profile));
}
else
{
const std::string id = entry["id"].get<std::string>();
if (auto loaded = loadProfileFromDisk(id))
{
catalog.push_back(LayoutProfile::catalogEntryFromProfile(*loaded));
}
else
{
profile = LayoutProfile::make(entry["name"].get<std::string>(), LayoutSchema::defaultLayoutObject(), nlohmann::json::array());
profile["id"] = id;
profile["created_at"] = entry.value("created_at", IdUtil::nowIso8601());
LayoutProfile::touch(profile);
saveProfileToDisk(profile);
catalog.push_back(LayoutProfile::catalogEntryFromProfile(profile));
}
}
}
s["layouts"] = catalog;
}
else
{
stripInlineProfiles(s);
if (!s.contains("active_layout_id") || !s["active_layout_id"].is_string() ||
!LayoutProfile::findIndex(s, s["active_layout_id"].get<std::string>()))
{
s["active_layout_id"] = s["layouts"][0]["id"].get<std::string>();
}
}
s["version"] = 3;
s.erase("layout");
s.erase("lidars");
loadActiveCache();
}
void StateRepository::bootstrapDefaultState()
{
const nlohmann::json layout = LayoutSchema::defaultLayoutObject();
nlohmann::json profile = LayoutProfile::make("Mặc định", layout, nlohmann::json::array());
LayoutSchema::ensure(profile["layout"]);
saveProfileToDisk(profile);
app_.state = nlohmann::json{{"version", 3},
{"active_layout_id", profile["id"]},
{"layouts", nlohmann::json::array({LayoutProfile::catalogEntryFromProfile(profile)})}};
app_.state["layout"] = profile["layout"];
app_.state["lidars"] = profile["lidars"];
app_.state["imus"] = profile.contains("imus") ? profile["imus"] : nlohmann::json::array();
}
StateRepository::StateRepository(std::filesystem::path data_path)
{
app_.data_path = std::move(data_path);
}
bool StateRepository::load()
{
const auto raw = FileUtil::readBinary(app_.data_path);
if (raw.empty())
{
bootstrapDefaultState();
save();
return true;
}
try
{
app_.state = nlohmann::json::parse(raw);
ensureSchema();
save();
return true;
}
catch (...)
{
bootstrapDefaultState();
save();
return false;
}
}
void StateRepository::ensureSchema()
{
migrateStorage();
}
bool StateRepository::saveProfile(const nlohmann::json& profile)
{
return saveProfileToDisk(profile);
}
void StateRepository::reloadActiveCache()
{
loadActiveCache();
}
bool StateRepository::deleteProfile(const std::string& id)
{
return deleteProfileFile(id);
}
std::optional<nlohmann::json> StateRepository::loadProfileById(const std::string& id) const
{
return loadProfileFromDisk(id);
}
bool StateRepository::saveAppState()
{
if (!persistActiveProfile())
return false;
return save();
}
bool StateRepository::save() const
{
try
{
const nlohmann::json disk = globalStateForDisk(app_.state);
auto raw = disk.dump(2);
raw.push_back('\n');
return FileUtil::writeBinaryAtomic(app_.data_path, raw);
}
catch (...)
{
return false;
}
}
} // namespace lm