// Records on the consumer side of the LEGACY generator — the wrapper every // C++ module actually gets for its dependencies (`--dep =`). // // A contract's `type Status { ... }` used to reach every C++ consumer as an // untyped bag: QVariant on the Qt surface, LogosMap on the lp one. The // caller then had to know the field names AND, for a `bstr` field, that the // value arrives as the canonical `{"_bytes": "..."}` envelope it must unwrap // itself — while Rust and the client-stub backend hand back a real struct. // // These assert on generated source text. That the emitted conversions compile // and round-trip (bytes tagged at every depth, uint64 above 2^32 intact) is // covered by generating a wrapper and building it — see the PR description. #include #include #include #include "generator_lib.h" namespace { QJsonObject field(const char* name, const char* type) { QJsonObject f; f["name"] = name; f["type"] = type; return f; } QJsonObject param(const char* name, const char* type) { QJsonObject p; p["name"] = name; p["type"] = type; return p; } QJsonObject method(const char* name, const char* returnType, const QJsonArray& params = {}) { QJsonObject m; m["name"] = name; m["returnType"] = returnType; m["isInvokable"] = true; m["parameters"] = params; return m; } // `type Status { port: uint, blob: bstr }` plus a record that nests it in // both container shapes. QJsonArray statusRecords() { QJsonObject status; status["name"] = "Status"; status["fields"] = QJsonArray{field("port", "qulonglong"), field("blob", "QByteArray")}; QJsonObject batch; batch["name"] = "Batch"; batch["fields"] = QJsonArray{field("label", "QString"), field("items", "QList"), field("tags", "QMap")}; return QJsonArray{status, batch}; } QJsonArray statusMethods() { return QJsonArray{ method("getStatus", "Status"), method("describeStatus", "QString", QJsonArray{param("s", "Status")}), method("listStatuses", "QList"), method("getBatch", "Batch"), }; } } // namespace // The Qt surface: a struct nested in the wrapper class, typed accessors, and // no QVariant anywhere a record is named. TEST(Records, QtWrapperExposesTheStruct) { const QString h = makeHeader("info_module", "InfoModule", statusMethods(), ApiStyle::Qt, {}, BindMode::Static, statusRecords()); // Nested, so two deps may each declare a `Status` in one consumer. EXPECT_TRUE(h.contains(" struct Status {")); EXPECT_TRUE(h.contains(" qulonglong port{};")); EXPECT_TRUE(h.contains(" QByteArray blob{};")); // Containers of records keep their element type. EXPECT_TRUE(h.contains(" QList items{};")); EXPECT_TRUE(h.contains(" QMap tags{};")); EXPECT_TRUE(h.contains("Status getStatus(logos::CallError* err = nullptr);")); EXPECT_TRUE(h.contains("QString describeStatus(const Status& s,")); EXPECT_TRUE(h.contains("QList listStatuses(")); // The old fallback is gone. EXPECT_FALSE(h.contains("QVariant getStatus(")); EXPECT_FALSE(h.contains("describeStatus(QVariant")); } // The lp surface spells the same records in std types — a universal // (Qt-free) module never sees a Qt name. TEST(Records, LpWrapperUsesStdFieldTypes) { for (ApiStyle style : {ApiStyle::Lp}) { const QString h = makeHeader("info_module", "InfoModule", statusMethods(), style, {}, BindMode::Static, statusRecords()); EXPECT_TRUE(h.contains(" uint64_t port{};")) << h.toStdString(); EXPECT_TRUE(h.contains(" std::vector blob{};")); EXPECT_TRUE(h.contains(" std::vector items{};")); EXPECT_TRUE(h.contains(" std::map tags{};")); EXPECT_TRUE(h.contains("std::vector listStatuses(")); // No LogosMap stand-in for a record. EXPECT_FALSE(h.contains("LogosMap getStatus(")); } // std::map needs its header on the lp surface. const QString lp = makeHeader("info_module", "InfoModule", statusMethods(), ApiStyle::Lp, {}, BindMode::Static, statusRecords()); EXPECT_TRUE(lp.contains("#include ")); } // A `bstr` field must ride the canonical tagged form at any depth — the // defect class that made a record-as-LogosMap actively wrong rather than // merely inconvenient. TEST(Records, BytesFieldsUseTheCanonicalEncoding) { const QString lp = makeSource("info_module", "InfoModule", "info_module_api.h", statusMethods(), ApiStyle::Lp, {}, BindMode::Static, statusRecords()); EXPECT_TRUE(lp.contains("__j[\"blob\"] = logos::bytesToJson(v.blob);")); EXPECT_TRUE(lp.contains("__out.blob = logos::jsonToBytes(w.at(\"blob\"));")); const QString qt = makeSource("info_module", "InfoModule", "info_module_api.h", statusMethods(), ApiStyle::Qt, {}, BindMode::Static, statusRecords()); EXPECT_TRUE(qt.contains("__out.blob = __m.value(QStringLiteral(\"blob\")).toByteArray();")); } // The conversions are file-local statics in the .cpp: an lp consumer's own // translation units must not need the wire type to include the header. TEST(Records, ConversionsStayOutOfTheHeader) { const QString h = makeHeader("info_module", "InfoModule", statusMethods(), ApiStyle::Lp, {}, BindMode::Static, statusRecords()); EXPECT_FALSE(h.contains("recToWire_Status")); const QString c = makeSource("info_module", "InfoModule", "info_module_api.h", statusMethods(), ApiStyle::Lp, {}, BindMode::Static, statusRecords()); EXPECT_TRUE(c.contains("static nlohmann::json recToWire_Status(const InfoModule::Status& v);")); EXPECT_TRUE(c.contains("static InfoModule::Status recFromWire_Status(const nlohmann::json& w);")); // Declared before defined, so records may reference each other in any order. EXPECT_LT(c.indexOf("static nlohmann::json recToWire_Batch(const InfoModule::Batch& v);"), c.indexOf("static nlohmann::json recToWire_Status(const InfoModule::Status& v) {")); } // A return type written before the `Class::` of a definition is outside class // scope and must be qualified; a parameter is inside it and must not be // (an unqualified return type simply does not compile). TEST(Records, ReturnTypesAreQualifiedInTheDefinition) { const QString c = makeSource("info_module", "InfoModule", "info_module_api.h", statusMethods(), ApiStyle::Qt, {}, BindMode::Static, statusRecords()); EXPECT_TRUE(c.contains("InfoModule::Status InfoModule::getStatus(")); EXPECT_TRUE(c.contains("QList InfoModule::listStatuses(")); EXPECT_TRUE(c.contains("QString InfoModule::describeStatus(const Status& s,")); } // Container decode lambdas are emitted INSIDE the record decoder, which has // its own `__m` / `__j`. Reusing those names made a map-of-records field read // from its own uninitialized local — it compiled, with only a warning. TEST(Records, ContainerLambdasDoNotShadowTheDecoderLocals) { for (ApiStyle style : {ApiStyle::Qt, ApiStyle::Lp}) { const QString c = makeSource("info_module", "InfoModule", "info_module_api.h", statusMethods(), style, {}, BindMode::Static, statusRecords()); // The decoder's own map is `__m` (Qt); a nested lambda must not // declare another one. EXPECT_FALSE(c.contains("const QVariantMap __m = (__m.value")) << c.toStdString(); EXPECT_FALSE(c.contains("const nlohmann::json& __j = w.at")) << c.toStdString(); } } // The record path is additive: with no records declared, every byte of the // generated output is what it was before. TEST(Records, EmptyRecordSetChangesNothing) { const QJsonArray methods{method("ping", "QString", QJsonArray{param("msg", "QString")})}; for (ApiStyle style : {ApiStyle::Qt, ApiStyle::Lp}) { EXPECT_EQ(makeHeader("m", "M", methods, style, {}, BindMode::Static, {}), makeHeader("m", "M", methods, style, {}, BindMode::Static)); EXPECT_EQ(makeSource("m", "M", "m_api.h", methods, style, {}, BindMode::Static, {}), makeSource("m", "M", "m_api.h", methods, style, {}, BindMode::Static)); } } // Records reach event callbacks too — an event payload is as typed as a // return value. TEST(Records, TypedEventCallbacksTakeTheStruct) { QJsonObject ev; ev["name"] = "statusChanged"; ev["params"] = QJsonArray{param("s", "Status"), param("at", "qlonglong")}; const QJsonArray events{ev}; const QString h = makeHeader("info_module", "InfoModule", statusMethods(), ApiStyle::Qt, events, BindMode::Static, statusRecords()); EXPECT_TRUE(h.contains("bool onStatusChanged(std::function callback);")); const QString c = makeSource("info_module", "InfoModule", "info_module_api.h", statusMethods(), ApiStyle::Qt, events, BindMode::Static, statusRecords()); EXPECT_TRUE(c.contains("recFromWire_Status(_args.at(0))")); }