384 lines
11 KiB
C++
384 lines
11 KiB
C++
#include "app.hpp"
|
||
|
||
#include <fstream>
|
||
#include <sstream>
|
||
#include <algorithm>
|
||
#include <cctype>
|
||
#include <cstdio>
|
||
#include <stdexcept>
|
||
|
||
// ============================================================
|
||
// 匿名命名空间:简易 JSON 序列化 / 反序列化
|
||
// 仅支持当前 TodoItem 数据结构的 JSON 子集,无外部依赖。
|
||
// ============================================================
|
||
namespace {
|
||
|
||
/**
|
||
* @brief 将字符串中的特殊字符转义为 JSON 安全形式。
|
||
*/
|
||
static std::string json_escape(const std::string& s) {
|
||
std::string r;
|
||
r.reserve(s.size() + 4);
|
||
for (char c : s) {
|
||
switch (c) {
|
||
case '"': r += "\\\""; break;
|
||
case '\\': r += "\\\\"; break;
|
||
case '\n': r += "\\n"; break;
|
||
case '\t': r += "\\t"; break;
|
||
case '\r': r += "\\r"; break;
|
||
default: r += c; break;
|
||
}
|
||
}
|
||
return r;
|
||
}
|
||
|
||
/**
|
||
* @brief 将 TodoItem 列表和 next_id 序列化为 JSON 字符串。
|
||
*/
|
||
static std::string items_to_json(const std::vector<TodoItem>& items, int next_id) {
|
||
std::ostringstream oss;
|
||
oss << "{\n \"items\": [\n";
|
||
for (size_t i = 0; i < items.size(); ++i) {
|
||
const auto& it = items[i];
|
||
oss << " {\n";
|
||
oss << " \"id\": " << it.id << ",\n";
|
||
oss << " \"title\": \"" << json_escape(it.title) << "\",\n";
|
||
oss << " \"description\": \"" << json_escape(it.description) << "\",\n";
|
||
oss << " \"completed\": " << (it.completed ? "true" : "false") << "\n";
|
||
oss << " }";
|
||
if (i + 1 < items.size()) oss << ",";
|
||
oss << "\n";
|
||
}
|
||
oss << " ],\n \"next_id\": " << next_id << "\n}\n";
|
||
return oss.str();
|
||
}
|
||
|
||
/**
|
||
* @brief 简易递归下降 JSON 解析器,专用于解析 TodoItem 数据格式。
|
||
*
|
||
* 支持 JSON 子集:对象、数组、字符串、整数、布尔值。
|
||
* 不依赖任何外部库。
|
||
*/
|
||
class JsonReader {
|
||
public:
|
||
explicit JsonReader(const std::string& input)
|
||
: s_(input), pos_(0) {}
|
||
|
||
/**
|
||
* @brief 解析顶层 JSON 对象,提取 items 和 next_id。
|
||
* @param[out] items 解析出的任务列表
|
||
* @param[out] next_id 解析出的下一个可用 ID
|
||
* @return 解析成功返回 true
|
||
*/
|
||
bool parse(std::vector<TodoItem>& items, int& next_id) {
|
||
items.clear();
|
||
skip_ws();
|
||
if (peek() != '{') return false;
|
||
advance(); // 跳过 '{'
|
||
|
||
while (pos_ < s_.size() && peek() != '}') {
|
||
skip_ws();
|
||
if (peek() == '}') break;
|
||
if (peek() == ',') { advance(); continue; }
|
||
|
||
std::string key = parse_string();
|
||
skip_ws();
|
||
if (peek() != ':') return false;
|
||
advance();
|
||
skip_ws();
|
||
|
||
if (key == "items") {
|
||
if (!parse_array(items)) return false;
|
||
} else if (key == "next_id") {
|
||
next_id = parse_int();
|
||
} else {
|
||
skip_value();
|
||
}
|
||
}
|
||
if (peek() == '}') advance();
|
||
return true;
|
||
}
|
||
|
||
private:
|
||
const std::string& s_;
|
||
size_t pos_ = 0;
|
||
|
||
/// @brief 查看当前字符,不移动指针。
|
||
char peek() const { return pos_ < s_.size() ? s_[pos_] : '\0'; }
|
||
|
||
/// @brief 读取当前字符并前进一位。
|
||
char advance() { return pos_ < s_.size() ? s_[pos_++] : '\0'; }
|
||
|
||
/// @brief 跳过空白字符。
|
||
void skip_ws() {
|
||
while (pos_ < s_.size() &&
|
||
std::isspace(static_cast<unsigned char>(s_[pos_]))) {
|
||
++pos_;
|
||
}
|
||
}
|
||
|
||
/// @brief 解析 JSON 字符串(含引号和转义处理)。
|
||
std::string parse_string() {
|
||
skip_ws();
|
||
if (peek() != '"') return {};
|
||
advance(); // 跳过开头的 "
|
||
std::string result;
|
||
while (pos_ < s_.size() && s_[pos_] != '"') {
|
||
if (s_[pos_] == '\\') {
|
||
advance();
|
||
if (pos_ < s_.size()) {
|
||
switch (s_[pos_]) {
|
||
case '"': result += '"'; break;
|
||
case '\\': result += '\\'; break;
|
||
case '/': result += '/'; break;
|
||
case 'n': result += '\n'; break;
|
||
case 't': result += '\t'; break;
|
||
case 'r': result += '\r'; break;
|
||
default: result += s_[pos_]; break;
|
||
}
|
||
}
|
||
} else {
|
||
result += s_[pos_];
|
||
}
|
||
++pos_;
|
||
}
|
||
if (peek() == '"') advance(); // 跳过结尾的 "
|
||
return result;
|
||
}
|
||
|
||
/// @brief 解析整数值(可选负号)。
|
||
int parse_int() {
|
||
skip_ws();
|
||
int sign = 1;
|
||
if (peek() == '-') { sign = -1; advance(); }
|
||
int val = 0;
|
||
while (pos_ < s_.size() &&
|
||
std::isdigit(static_cast<unsigned char>(s_[pos_]))) {
|
||
val = val * 10 + (s_[pos_] - '0');
|
||
++pos_;
|
||
}
|
||
return sign * val;
|
||
}
|
||
|
||
/**
|
||
* @brief 解析布尔值 true / false。
|
||
* @param[out] value 解析结果
|
||
* @return 解析成功返回 true
|
||
*/
|
||
bool parse_bool(bool& value) {
|
||
skip_ws();
|
||
if (s_.substr(pos_, 4) == "true") {
|
||
pos_ += 4;
|
||
value = true;
|
||
return true;
|
||
}
|
||
if (s_.substr(pos_, 5) == "false") {
|
||
pos_ += 5;
|
||
value = false;
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* @brief 解析 JSON 数组,元素为 TodoItem 对象。
|
||
*/
|
||
bool parse_array(std::vector<TodoItem>& items) {
|
||
skip_ws();
|
||
if (peek() != '[') return false;
|
||
advance(); // 跳过 '['
|
||
|
||
while (pos_ < s_.size() && peek() != ']') {
|
||
skip_ws();
|
||
if (peek() == ']') break;
|
||
if (peek() == ',') { advance(); continue; }
|
||
|
||
TodoItem item;
|
||
if (!parse_object(item)) return false;
|
||
items.push_back(std::move(item));
|
||
}
|
||
if (peek() == ']') advance();
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* @brief 解析单个 TodoItem JSON 对象。
|
||
*/
|
||
bool parse_object(TodoItem& item) {
|
||
skip_ws();
|
||
if (peek() != '{') return false;
|
||
advance(); // 跳过 '{'
|
||
|
||
while (pos_ < s_.size() && peek() != '}') {
|
||
skip_ws();
|
||
if (peek() == '}') break;
|
||
if (peek() == ',') { advance(); continue; }
|
||
|
||
std::string key = parse_string();
|
||
skip_ws();
|
||
if (peek() != ':') return false;
|
||
advance();
|
||
skip_ws();
|
||
|
||
if (key == "id") {
|
||
item.id = parse_int();
|
||
} else if (key == "title") {
|
||
item.title = parse_string();
|
||
} else if (key == "description") {
|
||
item.description = parse_string();
|
||
} else if (key == "completed") {
|
||
bool val = false;
|
||
if (parse_bool(val)) item.completed = val;
|
||
} else {
|
||
skip_value();
|
||
}
|
||
}
|
||
if (peek() == '}') advance();
|
||
return true;
|
||
}
|
||
|
||
/// @brief 跳过任意 JSON 值(用于忽略不关心的字段)。
|
||
void skip_value() {
|
||
skip_ws();
|
||
char c = peek();
|
||
if (c == '"') {
|
||
parse_string();
|
||
} else if (c == '{') {
|
||
int depth = 1;
|
||
advance();
|
||
while (pos_ < s_.size() && depth > 0) {
|
||
if (s_[pos_] == '{') ++depth;
|
||
else if (s_[pos_] == '}') --depth;
|
||
else if (s_[pos_] == '"') {
|
||
++pos_;
|
||
while (pos_ < s_.size() && s_[pos_] != '"') {
|
||
if (s_[pos_] == '\\') ++pos_;
|
||
++pos_;
|
||
}
|
||
}
|
||
++pos_;
|
||
}
|
||
} else if (c == '[') {
|
||
int depth = 1;
|
||
advance();
|
||
while (pos_ < s_.size() && depth > 0) {
|
||
if (s_[pos_] == '[') ++depth;
|
||
else if (s_[pos_] == ']') --depth;
|
||
else if (s_[pos_] == '"') {
|
||
++pos_;
|
||
while (pos_ < s_.size() && s_[pos_] != '"') {
|
||
if (s_[pos_] == '\\') ++pos_;
|
||
++pos_;
|
||
}
|
||
}
|
||
++pos_;
|
||
}
|
||
} else {
|
||
// 数字、布尔值、null 等
|
||
while (pos_ < s_.size() &&
|
||
!std::isspace(static_cast<unsigned char>(s_[pos_])) &&
|
||
s_[pos_] != ',' && s_[pos_] != '}' && s_[pos_] != ']') {
|
||
++pos_;
|
||
}
|
||
}
|
||
}
|
||
};
|
||
|
||
} // anonymous namespace
|
||
|
||
// ============================================================
|
||
// TodoService 实现
|
||
// ============================================================
|
||
|
||
TodoService::TodoService(const std::string& filepath)
|
||
: filepath_(filepath) {
|
||
load();
|
||
}
|
||
|
||
const std::vector<TodoItem>& TodoService::get_all() const {
|
||
return items_;
|
||
}
|
||
|
||
std::optional<TodoItem> TodoService::get_by_id(int id) const {
|
||
for (const auto& item : items_) {
|
||
if (item.id == id) return item;
|
||
}
|
||
return std::nullopt;
|
||
}
|
||
|
||
TodoItem TodoService::create(const std::string& title,
|
||
const std::string& description) {
|
||
TodoItem item;
|
||
item.id = next_id_++;
|
||
item.title = title;
|
||
item.description = description;
|
||
item.completed = false;
|
||
items_.push_back(item);
|
||
save();
|
||
return item;
|
||
}
|
||
|
||
std::optional<TodoItem> TodoService::update(
|
||
int id,
|
||
const std::optional<std::string>& title,
|
||
const std::optional<std::string>& description,
|
||
const std::optional<bool>& completed) {
|
||
for (auto& item : items_) {
|
||
if (item.id == id) {
|
||
if (title.has_value()) item.title = title.value();
|
||
if (description.has_value()) item.description = description.value();
|
||
if (completed.has_value()) item.completed = completed.value();
|
||
save();
|
||
return item;
|
||
}
|
||
}
|
||
return std::nullopt;
|
||
}
|
||
|
||
bool TodoService::delete_item(int id) {
|
||
auto it = std::find_if(items_.begin(), items_.end(),
|
||
[id](const TodoItem& item) { return item.id == id; });
|
||
if (it == items_.end()) return false;
|
||
items_.erase(it);
|
||
save();
|
||
return true;
|
||
}
|
||
|
||
void TodoService::load() {
|
||
std::ifstream ifs(filepath_);
|
||
if (!ifs.is_open()) {
|
||
items_.clear();
|
||
next_id_ = 1;
|
||
return;
|
||
}
|
||
std::stringstream buffer;
|
||
buffer << ifs.rdbuf();
|
||
std::string content = buffer.str();
|
||
if (content.empty()) {
|
||
items_.clear();
|
||
next_id_ = 1;
|
||
return;
|
||
}
|
||
|
||
items_.clear();
|
||
int parsed_next_id = 1;
|
||
JsonReader reader(content);
|
||
if (reader.parse(items_, parsed_next_id)) {
|
||
next_id_ = parsed_next_id;
|
||
// 确保 next_id 大于所有现有 ID,避免冲突
|
||
for (const auto& item : items_) {
|
||
if (item.id >= next_id_) next_id_ = item.id + 1;
|
||
}
|
||
} else {
|
||
// 解析失败时回退到空状态
|
||
items_.clear();
|
||
next_id_ = 1;
|
||
}
|
||
}
|
||
|
||
void TodoService::save() {
|
||
std::ofstream ofs(filepath_);
|
||
if (!ofs.is_open()) return;
|
||
ofs << items_to_json(items_, next_id_);
|
||
}
|