mirror of
https://github.com/logos-co/logos-protocol.git
synced 2026-08-30 21:41:10 +00:00
* Extract the Logos protocol layer from logos-cpp-sdk
Transports (plain TCP/TLS, qt_local, qt_remote/QRO, mock), token manager,
consumer core (LogosAPIClient/LogosAPIConsumer incl. the capability
auto-requestModule flow), ModuleProxy, the abstract LogosProviderObject
interface, and the canonical QVariant<->JSON conversion — now behind the
language-neutral lp_* C ABI (logos_protocol.h) carrying the protocol
semver (LOGOS_PROTOCOL_VERSION_*, lp_protocol_version()).
Bytes crossing the ABI use the lossless {"_bytes": base64url} tagging
(NUL-safe), matching the plain wire encoding.
Provider lp_* surface is compiled groundwork; serving lands with module
authoring.
* Move LogosProviderPlugin into logos_provider_interface.h
Plugin-loading tools (logos-cpp-generator's introspection mode, lm, the
hosts) need only qobject_cast<LogosProviderPlugin*>() + the abstract
LogosProviderObject — both framework-internal. Hosting the detection
interface here keeps those tools off the developer-facing logos-qt-sdk
layer. Same iid (org.logos.LogosProviderPlugin); header-only, ABI-neutral.
* Define the common module-impl C ABI (logos_module_impl.h)
ONE cdylib contract for module implementations in every language:
dispatch / get_methods / set_context / set_emit_callback / accept_token
/ get_protocol_version / string_free. The C++ and Rust SDKs emit these
exports around their respective impls; the uniform generated Qt glue
(and later a no-Qt host) talks to the cdylib only through this ABI.
JSON data model and tagged bytes form match the lp_* consumer ABI; the
protocol-version handshake complements the build-time metadata stamp.
* json convert: integers stay integers across the C ABI
QJsonValue::fromVariant degrades every numeric to double, so Int/UInt/
LongLong/ULongLong QVariants serialized as 5.0 — and a strict consumer on
the other side of the C ABI (a generated dispatch reading an int param)
rejects or zeroes them. Surfaced by the first cdylib-authored module
whose inbound args cross qvariantToNlohmann; the dlopen smoke harness
fed hand-written int JSON and never exercised this edge.
* call-error channel: surface {code,message,origin} for unacquirable targets
invokeRemoteMethod could not distinguish a failed call from a void/null
result — lp_invoke returned LP_OK with a null JSON result even when the
target module was never reached, and generated typed wrappers silently
defaulted (0 / empty string). Additive err-out overloads on
LogosAPIConsumer/LogosAPIClient fill a std-only logos::CallError
(logos_call_error.h, new LogosCallError exception for the generated
wrappers to throw); lp_invoke now honors its documented contract for
this class of failure: LP_ERR_UNAVAILABLE + canonical error JSON.
First detectable code: object_unavailable (requestObject failure) —
the struct is the extension point for transport-level statuses.
* call-error: drop the exception type — the error channel is the out-param
Per review, generated wrappers expose CallError as an optional trailing
out-parameter instead of throwing; the struct is the whole contract.
* ci: build + run the protocol test suite
On every pull request (unfiltered — stacked PRs included), master pushes,
and manual dispatch. The repo shipped without CI; its 111-test suite only
ran locally and through the workspace gate.
* consumer: typed requestModule for the capability flow
Port of logos-cpp-sdk master f5a127dd ('use updated capability module',
cpp-sdk#85, Iuri Matias) — the touched files (logos_api_client.cpp,
logos_api_consumer.{h,cpp}) moved into this repo in the P1 extraction.
The capability auto-requestModule path now calls a typed std::string
helper on the consumer (which acquires the capability object directly)
instead of a stringly invokeRemoteMethod round-trip. 111/111 tests.
* ci: DeterminateSystems nix installer (macOS runners)
cachix/install-nix-action fails on the macOS runners with
eDSRecordAlreadyExists (pre-existing nix build users); the org's
macOS-bearing workflows use the DeterminateSystems installer.
192 lines
7.2 KiB
C++
192 lines
7.2 KiB
C++
#include "logos_json_convert.h"
|
|
#include "logos_types.h"
|
|
#include <QJsonDocument>
|
|
#include <QJsonObject>
|
|
#include <QJsonArray>
|
|
#include <QJsonValue>
|
|
#include <QMetaType>
|
|
|
|
namespace logos {
|
|
|
|
namespace {
|
|
|
|
// Canonical C-ABI bytes encoding: {"_bytes": "<base64url, unpadded>"} — a
|
|
// single-key object, lossless for arbitrary bytes incl. embedded NUL.
|
|
// Matches the plain wire's encoding (implementations/plain/json_mapping.cpp);
|
|
// Qt's Base64UrlEncoding|OmitTrailingEquals produces the identical alphabet
|
|
// and padding-free form.
|
|
nlohmann::json byteArrayToTaggedJson(const QByteArray& bytes)
|
|
{
|
|
nlohmann::json obj;
|
|
obj["_bytes"] = bytes.toBase64(QByteArray::Base64UrlEncoding
|
|
| QByteArray::OmitTrailingEquals)
|
|
.toStdString();
|
|
return obj;
|
|
}
|
|
|
|
bool isTaggedBytes(const nlohmann::json& j)
|
|
{
|
|
return j.is_object() && j.size() == 1 && j.contains("_bytes")
|
|
&& j["_bytes"].is_string();
|
|
}
|
|
|
|
QByteArray taggedJsonToByteArray(const nlohmann::json& j)
|
|
{
|
|
return QByteArray::fromBase64(
|
|
QByteArray::fromStdString(j["_bytes"].get<std::string>()),
|
|
QByteArray::Base64UrlEncoding);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
nlohmann::json qvariantToNlohmann(const QVariant& v)
|
|
{
|
|
if (v.userType() == QMetaType::QByteArray)
|
|
return byteArrayToTaggedJson(v.toByteArray());
|
|
|
|
const int logosResultId = QMetaType::fromName("LogosResult").id();
|
|
if (logosResultId != QMetaType::UnknownType && v.userType() == logosResultId) {
|
|
const LogosResult lr = v.value<LogosResult>();
|
|
nlohmann::json obj;
|
|
obj["success"] = lr.success;
|
|
if (lr.value.userType() == QMetaType::QByteArray) {
|
|
// Bytes inside a LogosResult take the same tagged form — the
|
|
// QJsonValue::fromVariant fallback below would mangle them.
|
|
obj["value"] = byteArrayToTaggedJson(lr.value.toByteArray());
|
|
} else {
|
|
QJsonValue valJson = QJsonValue::fromVariant(lr.value);
|
|
if (valJson.isObject() || valJson.isArray()) {
|
|
QJsonDocument d = valJson.isObject() ? QJsonDocument(valJson.toObject())
|
|
: QJsonDocument(valJson.toArray());
|
|
try { obj["value"] = nlohmann::json::parse(d.toJson(QJsonDocument::Compact).toStdString()); }
|
|
catch (...) { obj["value"] = nullptr; }
|
|
} else if (valJson.isString()) obj["value"] = valJson.toString().toStdString();
|
|
else if (valJson.isBool()) obj["value"] = valJson.toBool();
|
|
else if (valJson.isDouble()) obj["value"] = valJson.toDouble();
|
|
else obj["value"] = nullptr;
|
|
}
|
|
QJsonValue errJson = QJsonValue::fromVariant(lr.error);
|
|
obj["error"] = errJson.isString() ? nlohmann::json(errJson.toString().toStdString())
|
|
: nullptr;
|
|
return obj;
|
|
}
|
|
|
|
if (v.canConvert<QJsonObject>()) {
|
|
QJsonDocument doc(v.toJsonObject());
|
|
try { return nlohmann::json::parse(doc.toJson(QJsonDocument::Compact).toStdString()); }
|
|
catch (...) {}
|
|
}
|
|
if (v.canConvert<QJsonArray>()) {
|
|
QJsonDocument doc(qvariant_cast<QJsonArray>(v));
|
|
try { return nlohmann::json::parse(doc.toJson(QJsonDocument::Compact).toStdString()); }
|
|
catch (...) {}
|
|
}
|
|
|
|
// Integers stay integers: QJsonValue::fromVariant degrades every numeric
|
|
// to double, which a strict consumer on the other side of the C ABI
|
|
// (e.g. a generated dispatch reading an int param) must not see as 5.0.
|
|
switch (v.userType()) {
|
|
case QMetaType::Int: return v.toInt();
|
|
case QMetaType::UInt: return v.toUInt();
|
|
case QMetaType::LongLong: return static_cast<int64_t>(v.toLongLong());
|
|
case QMetaType::ULongLong: return static_cast<uint64_t>(v.toULongLong());
|
|
default: break;
|
|
}
|
|
|
|
QJsonValue jv = QJsonValue::fromVariant(v);
|
|
if (jv.isString()) return jv.toString().toStdString();
|
|
if (jv.isBool()) return jv.toBool();
|
|
if (jv.isDouble()) return jv.toDouble();
|
|
if (jv.isObject() || jv.isArray()) {
|
|
QJsonDocument doc = jv.isObject() ? QJsonDocument(jv.toObject())
|
|
: QJsonDocument(jv.toArray());
|
|
try { return nlohmann::json::parse(doc.toJson(QJsonDocument::Compact).toStdString()); }
|
|
catch (...) {}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
QVariant nlohmannToQVariant(const nlohmann::json& j)
|
|
{
|
|
if (j.is_null())
|
|
return QVariant();
|
|
if (j.is_boolean())
|
|
return QVariant(j.get<bool>());
|
|
if (j.is_number_unsigned())
|
|
return QVariant(static_cast<qulonglong>(j.get<uint64_t>()));
|
|
if (j.is_number_integer())
|
|
return QVariant(static_cast<qlonglong>(j.get<int64_t>()));
|
|
if (j.is_number_float())
|
|
return QVariant(j.get<double>());
|
|
if (j.is_string())
|
|
return QVariant(QString::fromStdString(j.get<std::string>()));
|
|
if (isTaggedBytes(j))
|
|
return QVariant(taggedJsonToByteArray(j));
|
|
if (j.is_object()) {
|
|
QJsonDocument doc = QJsonDocument::fromJson(
|
|
QByteArray::fromStdString(j.dump()));
|
|
return QVariant::fromValue(doc.object());
|
|
}
|
|
if (j.is_array()) {
|
|
QJsonDocument doc = QJsonDocument::fromJson(
|
|
QByteArray::fromStdString(j.dump()));
|
|
return QVariant::fromValue(doc.array());
|
|
}
|
|
return QVariant();
|
|
}
|
|
|
|
QVariantList nlohmannArgsToQVariantList(const nlohmann::json& args)
|
|
{
|
|
QVariantList result;
|
|
if (!args.is_array()) return result;
|
|
for (const auto& arg : args) {
|
|
if (arg.is_string())
|
|
result.append(QString::fromStdString(arg.get<std::string>()));
|
|
else if (arg.is_boolean())
|
|
result.append(arg.get<bool>());
|
|
else if (arg.is_number_unsigned())
|
|
result.append(static_cast<qulonglong>(arg.get<uint64_t>()));
|
|
else if (arg.is_number_integer())
|
|
result.append(static_cast<qlonglong>(arg.get<int64_t>()));
|
|
else if (arg.is_number_float())
|
|
result.append(arg.get<double>());
|
|
else if (arg.is_null())
|
|
result.append(QVariant());
|
|
else if (isTaggedBytes(arg))
|
|
result.append(QVariant(taggedJsonToByteArray(arg)));
|
|
else if (arg.is_object() || arg.is_array()) {
|
|
QJsonDocument doc = QJsonDocument::fromJson(
|
|
QByteArray::fromStdString(arg.dump()));
|
|
result.append(arg.is_object()
|
|
? QVariant::fromValue(doc.object())
|
|
: QVariant::fromValue(doc.array()));
|
|
} else {
|
|
result.append(QVariant());
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
QJsonArray methodsToJsonArray(const std::vector<LogosMethodMetadata>& methods)
|
|
{
|
|
QJsonArray out;
|
|
for (const auto& m : methods) {
|
|
QJsonObject o;
|
|
o["name"] = QString::fromStdString(m.name);
|
|
o["signature"] = QString::fromStdString(m.signature);
|
|
o["returnType"] = QString::fromStdString(m.returnType);
|
|
o["isInvokable"] = m.isInvokable;
|
|
if (m.parameters.is_array()) {
|
|
QJsonDocument paramDoc = QJsonDocument::fromJson(
|
|
QByteArray::fromStdString(m.parameters.dump()));
|
|
o["parameters"] = paramDoc.array();
|
|
} else {
|
|
o["parameters"] = QJsonArray();
|
|
}
|
|
out.append(o);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
} // namespace logos
|