/* * api_client.cpp — Реализация ApiClient (Singleton + HTTP + Adapter) * * Жизненный цикл одного запроса (пример fetchQuizzes): * * 1. fetchQuizzes() создаёт QNetworkRequest и вызывает m_manager->get() * 2. QNetworkAccessManager отправляет HTTP GET асинхронно * (не блокирует event loop) * 3. Когда ответ получен, Qt вызывает лямбду, подключённую к * сигналу QNetworkReply::finished * 4. Лямбда читает байты через reply->readAll() * 5. QJsonDocument::fromJson() парсит байты в дерево JSON * 6. m_adapter->toQuizList() десериализует дерево в Quiz-объекты * 7. emit quizzesReceived(...) уведомляет подписчиков */ #include "api_client.h" #include "quiz_json_adapter.h" #include // результат асинхронного HTTP-запроса #include // описание запроса: URL, заголовки #include // точка входа в парсер JSON #include // JSON-массив [ ... ] #include // JSON-объект { ... } #include // ───────────────────────────────────────────────────────────────────────────── // Реализация паттерна Синглтон // ───────────────────────────────────────────────────────────────────────────── ApiClient &ApiClient::instance() { /* * Meyers' Singleton: статическая локальная переменная. * C++11 гарантирует потокобезопасность инициализации. * Хранится как статическое свойство функции — одна на всё приложение. */ static ApiClient s_instance; return s_instance; } ApiClient::ApiClient(QObject *parent) : QObject(parent) , m_manager(new QNetworkAccessManager(this)) , m_adapter(new QuizJsonAdapter()) , m_baseUrl("http://localhost:80") { } ApiClient::~ApiClient() { delete m_adapter; } // ───────────────────────────────────────────────────────────────────────────── // fetchQuizzes — GET /api/quiz/ // ───────────────────────────────────────────────────────────────────────────── void ApiClient::fetchQuizzes() { QNetworkRequest request(QUrl(m_baseUrl + "/api/quiz/")); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); QNetworkReply *reply = m_manager->get(request); connect(reply, &QNetworkReply::finished, this, [this, reply]() { reply->deleteLater(); if (reply->error() != QNetworkReply::NoError) { emit errorOccurred(reply->errorString()); return; } // reply->readAll() — читаем весь буфер HTTP-ответа QByteArray rawData = reply->readAll(); QJsonDocument doc = QJsonDocument::fromJson(rawData); if (!doc.isArray()) { emit errorOccurred("Unexpected JSON format: array expected"); return; } QList quizzes = m_adapter->toQuizList(doc.array()); emit quizzesReceived(quizzes); }); } // ───────────────────────────────────────────────────────────────────────────── // fetchQuiz — GET /api/quiz/:id/ // ───────────────────────────────────────────────────────────────────────────── void ApiClient::fetchQuiz(int id) { QNetworkRequest request(QUrl(m_baseUrl + "/api/quiz/" + QString::number(id) + "/")); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); QNetworkReply *reply = m_manager->get(request); connect(reply, &QNetworkReply::finished, this, [this, reply]() { reply->deleteLater(); if (reply->error() != QNetworkReply::NoError) { emit errorOccurred(reply->errorString()); return; } QByteArray rawData = reply->readAll(); QJsonDocument doc = QJsonDocument::fromJson(rawData); if (!doc.isObject()) { emit errorOccurred("Unexpected JSON format: object expected"); return; } emit quizReceived(m_adapter->toQuiz(doc.object())); }); } // ───────────────────────────────────────────────────────────────────────────── // createQuiz — POST /api/quiz/ // ───────────────────────────────────────────────────────────────────────────── void ApiClient::createQuiz(const QJsonObject &data) { QNetworkRequest request(QUrl(m_baseUrl + "/api/quiz/")); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); QByteArray body = QJsonDocument(data).toJson(QJsonDocument::Compact); QNetworkReply *reply = m_manager->post(request, body); connect(reply, &QNetworkReply::finished, this, [this, reply]() { reply->deleteLater(); if (reply->error() != QNetworkReply::NoError) { emit errorOccurred(reply->errorString()); return; } QByteArray rawData = reply->readAll(); QJsonDocument doc = QJsonDocument::fromJson(rawData); if (!doc.isObject()) { emit errorOccurred("POST: unexpected JSON format"); return; } emit quizCreated(m_adapter->toQuiz(doc.object())); }); } // ───────────────────────────────────────────────────────────────────────────── // updateQuiz — PUT /api/quiz/:id/ // ───────────────────────────────────────────────────────────────────────────── void ApiClient::updateQuiz(int id, const QJsonObject &data) { QNetworkRequest request(QUrl(m_baseUrl + "/api/quiz/" + QString::number(id) + "/")); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); QByteArray body = QJsonDocument(data).toJson(QJsonDocument::Compact); QNetworkReply *reply = m_manager->put(request, body); connect(reply, &QNetworkReply::finished, this, [this, reply]() { reply->deleteLater(); if (reply->error() != QNetworkReply::NoError) { emit errorOccurred(reply->errorString()); return; } QByteArray rawData = reply->readAll(); QJsonDocument doc = QJsonDocument::fromJson(rawData); if (!doc.isObject()) { emit errorOccurred("PUT: unexpected JSON format"); return; } emit quizUpdated(m_adapter->toQuiz(doc.object())); }); } // ───────────────────────────────────────────────────────────────────────────── // deleteQuiz — DELETE /api/quiz/:id/ // ───────────────────────────────────────────────────────────────────────────── void ApiClient::deleteQuiz(int id) { QNetworkRequest request(QUrl(m_baseUrl + "/api/quiz/" + QString::number(id) + "/")); QNetworkReply *reply = m_manager->deleteResource(request); connect(reply, &QNetworkReply::finished, this, [this, reply]() { reply->deleteLater(); if (reply->error() != QNetworkReply::NoError) { emit errorOccurred(reply->errorString()); return; } QByteArray rawData = reply->readAll(); QJsonDocument doc = QJsonDocument::fromJson(rawData); QString message = "Тест удалён"; if (doc.isObject() && doc.object().contains("detail")) message = doc.object()["detail"].toString(); emit quizDeleted(message); }); }