feat/dataset-detail-chart #5
|
|
@ -7,7 +7,8 @@ add_library(geopro_data STATIC
|
|||
dto/DatasetChartDto.cpp
|
||||
api/ApiProjectRepository.cpp
|
||||
api/ApiDatasetRepository.cpp
|
||||
api/DatasetLoadHandles.cpp)
|
||||
api/DatasetLoadHandles.cpp
|
||||
api/NavRequest.cpp)
|
||||
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_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_dataset_chart_dto.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)
|
||||
|
||||
# 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