mirror of
https://github.com/logos-co/logos-protocol.git
synced 2026-08-27 12:01:15 +00:00
feat(json-convert): jsonToLogosResult — the missing inverse of a converter we already had (#35)
qvariantToNlohmann has always owned LogosResult -> {success,value,error}. The
way back did not exist: nlohmannToQVariant turns that object into a plain
QVariantMap, and a qvariant_cast<LogosResult> of a QVariantMap yields a
default-constructed, silently-failed result. So every consumer that received a
`result` over the canonical JSON wire either re-derived the decode or lost it.
The pair is now symmetric, and both fields recurse through the canonical
decoder — so a `value` carrying bytes / 64-bit integers / containers comes back
with the shape the encoder sent, and a null `error` stays an INVALID QVariant
rather than becoming an empty QString. That last state is the point: it is what
the Qt transport delivers for "no error", and no std::string-typed intermediate
can carry it.
Tests pin the round trip, the absent-error state, bytes + uint64 inside `value`,
and the non-object input.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
4ee85b26a6
commit
ec43a0b441
@@ -158,6 +158,22 @@ QVariant nlohmannToQVariant(const nlohmann::json& j)
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
LogosResult jsonToLogosResult(const nlohmann::json& j)
|
||||
{
|
||||
LogosResult r;
|
||||
r.success = false;
|
||||
if (!j.is_object()) return r;
|
||||
if (j.contains("success") && j["success"].is_boolean())
|
||||
r.success = j["success"].get<bool>();
|
||||
// Both payload fields recurse through the canonical decoder, so a `value`
|
||||
// carrying bytes / nested integers / containers comes back with the same
|
||||
// shape qvariantToNlohmann sent, and a null `error` stays an invalid
|
||||
// QVariant rather than becoming an empty QString.
|
||||
if (j.contains("value")) r.value = nlohmannToQVariant(j["value"]);
|
||||
if (j.contains("error")) r.error = nlohmannToQVariant(j["error"]);
|
||||
return r;
|
||||
}
|
||||
|
||||
QVariantList nlohmannArgsToQVariantList(const nlohmann::json& args)
|
||||
{
|
||||
QVariantList result;
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "logos_types.h" // LogosResult
|
||||
|
||||
struct LogosMethodMetadata {
|
||||
std::string name;
|
||||
std::string signature;
|
||||
@@ -20,6 +22,24 @@ namespace logos {
|
||||
|
||||
nlohmann::json qvariantToNlohmann(const QVariant& v);
|
||||
QVariant nlohmannToQVariant(const nlohmann::json& j);
|
||||
|
||||
// The inverse of the LogosResult branch inside qvariantToNlohmann.
|
||||
//
|
||||
// Without it the pair is asymmetric: qvariantToNlohmann OWNS
|
||||
// LogosResult -> {success,value,error}, but the return direction only had
|
||||
// nlohmannToQVariant, which turns that object into a plain QVariantMap — and
|
||||
// `qvariant_cast<LogosResult>` of a QVariantMap yields a default-constructed
|
||||
// (success=false) result, silently. Any consumer that receives a `result` over
|
||||
// the canonical JSON wire needs this, so it lives beside its inverse rather
|
||||
// than being re-derived per code generator.
|
||||
//
|
||||
// `error` is decoded through nlohmannToQVariant so a JSON null stays an INVALID
|
||||
// QVariant — matching what the Qt transport delivers for "no error" and what
|
||||
// the encoder above emits. A std::string-typed intermediate cannot represent
|
||||
// that state, which is why this is the JSON-level inverse and not a
|
||||
// StdLogosResult hop.
|
||||
LogosResult jsonToLogosResult(const nlohmann::json& j);
|
||||
|
||||
QVariantList nlohmannArgsToQVariantList(const nlohmann::json& args);
|
||||
QJsonArray methodsToJsonArray(const std::vector<LogosMethodMetadata>& methods);
|
||||
|
||||
|
||||
@@ -5,6 +5,11 @@ add_executable(protocol_tests
|
||||
# New protocol-level tests
|
||||
test_protocol_version.cpp
|
||||
test_json_convert_bytes.cpp
|
||||
# The `result` half of the canonical converter pair: qvariantToNlohmann has
|
||||
# always owned LogosResult -> JSON; jsonToLogosResult is its inverse, and
|
||||
# the state it exists to protect is an ABSENT error (which no
|
||||
# std::string-typed intermediate can carry).
|
||||
test_json_convert_result.cpp
|
||||
test_universal_provider_dispatch.cpp
|
||||
# Event payload fidelity across the universal -> Qt bridge
|
||||
# (setEventListenerStdBridge). Every Qt-free provider emits through that one
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "logos_json_convert.h"
|
||||
#include "logos_types.h"
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QMetaType>
|
||||
#include <QVariant>
|
||||
#include <QVariantMap>
|
||||
|
||||
// `result` is the one LIDL type whose canonical converter pair used to be
|
||||
// ONE-WAY: qvariantToNlohmann owned LogosResult -> {success,value,error},
|
||||
// but nothing owned the way back, so every consumer that needed it either
|
||||
// re-derived it or silently lost the result (a `qvariant_cast<LogosResult>`
|
||||
// of the decoded QVariantMap default-constructs to success=false).
|
||||
//
|
||||
// These tests pin the inverse and, in particular, the two states a
|
||||
// std::string-typed intermediate cannot represent: an ABSENT error, and a
|
||||
// `value` whose shape must survive (bytes / 64-bit integers / containers).
|
||||
|
||||
using logos::qvariantToNlohmann;
|
||||
using logos::nlohmannToQVariant;
|
||||
using logos::jsonToLogosResult;
|
||||
|
||||
namespace {
|
||||
LogosResult makeResult(bool success, QVariant value, QVariant error)
|
||||
{
|
||||
LogosResult r;
|
||||
r.success = success;
|
||||
r.value = std::move(value);
|
||||
r.error = std::move(error);
|
||||
return r;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TEST(JsonConvertResult, RoundTripsSuccessWithAbsentError)
|
||||
{
|
||||
qRegisterMetaType<LogosResult>("LogosResult");
|
||||
|
||||
QVariantMap payload;
|
||||
payload.insert("id", QStringLiteral("abc"));
|
||||
payload.insert("count", QVariant(qlonglong(42)));
|
||||
|
||||
const LogosResult original = makeResult(true, QVariant(payload), QVariant());
|
||||
const nlohmann::json j = qvariantToNlohmann(QVariant::fromValue(original));
|
||||
|
||||
ASSERT_TRUE(j.is_object());
|
||||
EXPECT_TRUE(j["success"].get<bool>());
|
||||
EXPECT_TRUE(j["error"].is_null());
|
||||
|
||||
const LogosResult back = jsonToLogosResult(j);
|
||||
EXPECT_TRUE(back.success);
|
||||
// The absence of an error must survive: an empty QString here would make
|
||||
// `error.isValid()` true and change what every caller sees.
|
||||
EXPECT_FALSE(back.error.isValid());
|
||||
ASSERT_TRUE(back.value.canConvert<QVariantMap>());
|
||||
const QVariantMap m = back.value.toMap();
|
||||
EXPECT_EQ(m.value("id").toString(), QStringLiteral("abc"));
|
||||
EXPECT_EQ(m.value("count").toLongLong(), 42);
|
||||
}
|
||||
|
||||
TEST(JsonConvertResult, RoundTripsFailureWithErrorString)
|
||||
{
|
||||
qRegisterMetaType<LogosResult>("LogosResult");
|
||||
|
||||
const LogosResult original =
|
||||
makeResult(false, QVariant(), QVariant(QStringLiteral("boom")));
|
||||
const nlohmann::json j = qvariantToNlohmann(QVariant::fromValue(original));
|
||||
|
||||
const LogosResult back = jsonToLogosResult(j);
|
||||
EXPECT_FALSE(back.success);
|
||||
EXPECT_EQ(back.error.toString(), QStringLiteral("boom"));
|
||||
EXPECT_FALSE(back.value.isValid());
|
||||
}
|
||||
|
||||
TEST(JsonConvertResult, ValueKeepsBytesAndUint64)
|
||||
{
|
||||
qRegisterMetaType<LogosResult>("LogosResult");
|
||||
|
||||
QVariantMap payload;
|
||||
payload.insert("blob", QVariant(QByteArray("\x00\x80\xff", 3)));
|
||||
payload.insert("big", QVariant(qulonglong(18446744073709551615ULL)));
|
||||
|
||||
const LogosResult original = makeResult(true, QVariant(payload), QVariant());
|
||||
const LogosResult back = jsonToLogosResult(qvariantToNlohmann(QVariant::fromValue(original)));
|
||||
|
||||
const QVariantMap m = back.value.toMap();
|
||||
// Bytes stay bytes (tagged form decoded back to a QByteArray), not a
|
||||
// base64 string and not a one-key map.
|
||||
ASSERT_EQ(m.value("blob").userType(), int(QMetaType::QByteArray));
|
||||
EXPECT_EQ(m.value("blob").toByteArray().size(), 3);
|
||||
EXPECT_EQ(m.value("big").toULongLong(), 18446744073709551615ULL);
|
||||
}
|
||||
|
||||
TEST(JsonConvertResult, NonObjectDecodesToFailure)
|
||||
{
|
||||
const LogosResult back = jsonToLogosResult(nlohmann::json("not-a-result"));
|
||||
EXPECT_FALSE(back.success);
|
||||
EXPECT_FALSE(back.value.isValid());
|
||||
EXPECT_FALSE(back.error.isValid());
|
||||
}
|
||||
Reference in New Issue
Block a user