feat(data): NavRequest 单请求异步句柄(QVariant payload, abort闸门) + 元类型声明 + 离线单测
This commit is contained in:
parent
22a7f2339e
commit
4beb7a9523
|
|
@ -7,7 +7,8 @@ add_library(geopro_data STATIC
|
||||||
dto/DatasetChartDto.cpp
|
dto/DatasetChartDto.cpp
|
||||||
api/ApiProjectRepository.cpp
|
api/ApiProjectRepository.cpp
|
||||||
api/ApiDatasetRepository.cpp
|
api/ApiDatasetRepository.cpp
|
||||||
api/DatasetLoadHandles.cpp)
|
api/DatasetLoadHandles.cpp
|
||||||
|
api/NavRequest.cpp)
|
||||||
target_include_directories(geopro_data PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
target_include_directories(geopro_data PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
||||||
target_link_libraries(geopro_data PUBLIC geopro_core geopro_net Qt6::Core PRIVATE nlohmann_json::nlohmann_json)
|
target_link_libraries(geopro_data PUBLIC geopro_core geopro_net Qt6::Core PRIVATE nlohmann_json::nlohmann_json)
|
||||||
target_compile_features(geopro_data PUBLIC cxx_std_17)
|
target_compile_features(geopro_data PUBLIC cxx_std_17)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
#pragma once
|
||||||
|
#include <vector>
|
||||||
|
#include <QMetaType>
|
||||||
|
#include "repo/RepoTypes.hpp"
|
||||||
|
|
||||||
|
// 导航异步返回类型经 QVariant 承载:同线程直连仅需 Q_DECLARE_METATYPE(无需 qRegisterMetaType)。
|
||||||
|
Q_DECLARE_METATYPE(std::vector<geopro::data::Workspace>)
|
||||||
|
Q_DECLARE_METATYPE(geopro::data::ProjectListPage)
|
||||||
|
Q_DECLARE_METATYPE(std::vector<geopro::data::ProjectType>)
|
||||||
|
Q_DECLARE_METATYPE(std::vector<geopro::data::StructNode>)
|
||||||
|
Q_DECLARE_METATYPE(geopro::data::DsPage)
|
||||||
|
Q_DECLARE_METATYPE(geopro::data::DynamicForm)
|
||||||
|
Q_DECLARE_METATYPE(std::vector<geopro::data::ExceptionRow>)
|
||||||
|
// bool 已内置 QMetaType。
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
#include "api/NavRequest.hpp"
|
||||||
|
#include <stdexcept>
|
||||||
|
|
||||||
|
namespace geopro::data {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
QString reasonOf(const geopro::net::ApiResponse& r) {
|
||||||
|
return r.msg.isEmpty() ? r.rawError : r.msg;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
ApiNavRequest::ApiNavRequest(geopro::net::IApiCall* call, Parser parse, Predicate isFailure,
|
||||||
|
QObject* parent)
|
||||||
|
: NavRequest(parent), call_(call), parse_(std::move(parse)), isFailure_(std::move(isFailure)) {
|
||||||
|
QObject::connect(call, &geopro::net::IApiCall::finished, this,
|
||||||
|
[this](const geopro::net::ApiResponse& resp) {
|
||||||
|
if (aborted_) return; // §5.0 入口守卫
|
||||||
|
if (isFailure_(resp)) {
|
||||||
|
emit failed(reasonOf(resp));
|
||||||
|
deleteLater();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
QVariant out;
|
||||||
|
try {
|
||||||
|
out = parse_(resp); // 仅解析在 try 内(下游 done 处理器抛出不误报)
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
emit failed(QString::fromUtf8(e.what()));
|
||||||
|
deleteLater();
|
||||||
|
return;
|
||||||
|
} catch (...) {
|
||||||
|
emit failed(QStringLiteral("解析失败:未知异常"));
|
||||||
|
deleteLater();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
emit done(out);
|
||||||
|
deleteLater();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void ApiNavRequest::abort() {
|
||||||
|
if (aborted_) return;
|
||||||
|
aborted_ = true;
|
||||||
|
if (call_) call_->abort();
|
||||||
|
deleteLater();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace geopro::data
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
#pragma once
|
||||||
|
#include <functional>
|
||||||
|
#include <QObject>
|
||||||
|
#include <QPointer>
|
||||||
|
#include <QString>
|
||||||
|
#include <QVariant>
|
||||||
|
#include "IApiCall.hpp"
|
||||||
|
|
||||||
|
namespace geopro::data {
|
||||||
|
|
||||||
|
// 单请求异步句柄(抽象基,可测试缝):payload 经 QVariant 承载,控制器侧 qvariant_cast<T> 取出。
|
||||||
|
class NavRequest : public QObject {
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
using QObject::QObject;
|
||||||
|
~NavRequest() override = default;
|
||||||
|
virtual void abort() = 0;
|
||||||
|
signals:
|
||||||
|
void done(const QVariant& value);
|
||||||
|
void failed(const QString& message);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Api 实现:包一个 IApiCall + 注入的解析器(ApiResponse → QVariant)+ 失败谓词。
|
||||||
|
class ApiNavRequest : public NavRequest {
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
using Parser = std::function<QVariant(const geopro::net::ApiResponse&)>;
|
||||||
|
using Predicate = std::function<bool(const geopro::net::ApiResponse&)>;
|
||||||
|
ApiNavRequest(geopro::net::IApiCall* call, Parser parse, Predicate isFailure,
|
||||||
|
QObject* parent = nullptr); // 接管 call
|
||||||
|
void abort() override;
|
||||||
|
private:
|
||||||
|
QPointer<geopro::net::IApiCall> call_;
|
||||||
|
Parser parse_;
|
||||||
|
Predicate isFailure_;
|
||||||
|
bool aborted_ = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace geopro::data
|
||||||
|
|
@ -39,6 +39,8 @@ target_sources(geopro_tests PRIVATE data/test_local_repo.cpp)
|
||||||
target_sources(geopro_tests PRIVATE data/test_nav_dto.cpp)
|
target_sources(geopro_tests PRIVATE data/test_nav_dto.cpp)
|
||||||
target_sources(geopro_tests PRIVATE data/test_dataset_chart_dto.cpp)
|
target_sources(geopro_tests PRIVATE data/test_dataset_chart_dto.cpp)
|
||||||
target_sources(geopro_tests PRIVATE data/test_dataset_load_handles.cpp)
|
target_sources(geopro_tests PRIVATE data/test_dataset_load_handles.cpp)
|
||||||
|
# NavRequest 离线单测(QVariant payload: done/failed/abort 闸门)。
|
||||||
|
target_sources(geopro_tests PRIVATE data/test_nav_request.cpp)
|
||||||
target_link_libraries(geopro_tests PRIVATE geopro_data)
|
target_link_libraries(geopro_tests PRIVATE geopro_data)
|
||||||
|
|
||||||
# net 层:RSA 加密器。测试需直接用 OpenSSL 生成/解密密钥,故显式 find_package
|
# net 层:RSA 加密器。测试需直接用 OpenSSL 生成/解密密钥,故显式 find_package
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
#include <QSignalSpy>
|
||||||
|
#include <QVariant>
|
||||||
|
#include "api/NavRequest.hpp"
|
||||||
|
#include "api/NavLoads.hpp"
|
||||||
|
#include "net/FakeApiCall.hpp"
|
||||||
|
|
||||||
|
using namespace geopro::data;
|
||||||
|
using geopro::net::ApiResponse;
|
||||||
|
using geopro::net::test::FakeApiCall;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
ApiResponse ok() { ApiResponse r; r.code = 200; r.httpStatus = 200; return r; }
|
||||||
|
ApiResponse bad() { ApiResponse r; r.code = 500; r.httpStatus = 200; r.msg = QStringLiteral("boom"); return r; }
|
||||||
|
auto isFailure = [](const ApiResponse& r) { return r.code != 200 || !r.rawError.isEmpty(); };
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST(NavRequest, EmitsDoneWithParsedPayload) {
|
||||||
|
auto* call = new FakeApiCall;
|
||||||
|
auto* req = new ApiNavRequest(call,
|
||||||
|
[](const ApiResponse&) { return QVariant::fromValue(DsPage{{}, 42}); }, isFailure);
|
||||||
|
QSignalSpy doneSpy(req, &NavRequest::done);
|
||||||
|
call->fire(ok());
|
||||||
|
ASSERT_EQ(doneSpy.count(), 1);
|
||||||
|
const auto page = qvariant_cast<DsPage>(doneSpy.takeFirst().at(0));
|
||||||
|
EXPECT_EQ(page.total, 42);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(NavRequest, EmitsFailedOnBusinessError) {
|
||||||
|
auto* call = new FakeApiCall;
|
||||||
|
auto* req = new ApiNavRequest(call, [](const ApiResponse&) { return QVariant(); }, isFailure);
|
||||||
|
QSignalSpy failSpy(req, &NavRequest::failed);
|
||||||
|
call->fire(bad());
|
||||||
|
EXPECT_EQ(failSpy.count(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(NavRequest, AbortSuppressesLateDone) {
|
||||||
|
auto* call = new FakeApiCall;
|
||||||
|
auto* req = new ApiNavRequest(call, [](const ApiResponse&) { return QVariant(); }, isFailure);
|
||||||
|
QSignalSpy doneSpy(req, &NavRequest::done);
|
||||||
|
req->abort();
|
||||||
|
EXPECT_TRUE(call->aborted);
|
||||||
|
call->fire(ok());
|
||||||
|
EXPECT_EQ(doneSpy.count(), 0);
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue