mirror of
https://github.com/logos-co/logos-cpp-sdk.git
synced 2026-08-31 01:31:10 +00:00
feat(records): typed structs for C++ dependency wrappers
Closes the second record gap. The wrapper every C++ module actually gets for
its dependencies comes from the LEGACY generator (`--dep <name>=<lidl>`), not
the client-stub backend the previous commit taught about records — and there a
contract's `type Status { ... }` reached the consumer as an untyped bag:
QVariant on the Qt surface, LogosMap on the std/lp one. Worse than
inconvenient for a `bstr` field: the caller received the canonical
`{"_bytes": "..."}` envelope and had to know to unwrap it, while Rust and the
stub backend handed back real bytes.
Now, on all three api styles:
struct Status { uint64_t port{}; std::vector<uint8_t> blob{}; };
Status getStatus(logos::CallError* err = nullptr);
std::string describeStatus(const Status& s, ...);
std::vector<Status> listStatuses(...);
The struct is NESTED in the wrapper class (`InfoModule::Status`) because a
module consuming two deps that each declare a `Status` includes both wrappers
into one translation unit. Conversions are file-local statics in the generated
.cpp, so a Qt-free module's own TUs still never see QVariant or nlohmann.
Records reach parameters, returns, event callbacks, `[Record]` and
`{tstr: Record}` — at any depth, with bytes tagged throughout.
Same commit, the legacy path's half of the 64-bit fix: `lidlTypeExprToQtTypeName`
mapped BOTH int and uint to `int` ("wire-as-int for now"), so a `uint` method on
a dep reached a Qt consumer as a signed 32-bit value and a std/lp consumer as a
signed int64_t. Now qlonglong/qulonglong, matching the spelling the other half
of this PR gave the stub backend. `lpFromJsonExpr` grew the uint64_t branch it
needed — without it a mistyped payload THREW out of nlohmann's implicit
conversion instead of defaulting like every other scalar.
Verified by generating a contract with a record, a record-of-records, a `uint`
above 2^32 and a high-byte `bstr`, then COMPILING the output for qt/std/lp and
round-tripping the emitted conversions:
- `{"_bytes":"gAH_"}` at every depth, decoding back to the same bytes
- 4294967296 intact through both directions
- garbage/missing fields default rather than throw
That compile is what caught the one real bug here: the container decode lambdas
declared `__m`/`__j`, shadowing the record decoder's own locals, so a
map-of-records field read from its own uninitialized local — it compiled with
nothing but a -Wuninitialized warning. Locals are `__acc`/`__src` now, pinned
by a test.
Also: 8 generator tests (one asserting an empty record set leaves every byte
of the output as it was), 179 total green; logos-test-modules builds and tests
green against this generator.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
32ab1c6504
commit
c598f569fd
@@ -192,10 +192,171 @@ static QString qVariantToStdReturn(const QString& qtType, const QString& varExpr
|
||||
return varExpr + ".toString().toStdString()";
|
||||
}
|
||||
|
||||
// ─── Records ─────────────────────────────────────────────────────────────
|
||||
//
|
||||
// A contract's `type Status { port: uint }` is a REAL C++ struct on the
|
||||
// consumer side, not a QVariant / LogosMap the caller picks apart by string
|
||||
// key. Without this a `bstr` field is the worst case: the caller receives the
|
||||
// canonical `{"_bytes": "..."}` envelope and has to know to unwrap it, while
|
||||
// every other language's consumer hands back plain bytes.
|
||||
//
|
||||
// main.cpp passes the declarations alongside the methods:
|
||||
// [ { "name": "Status", "fields": [ { "name": "port", "type": "qulonglong" } ] } ]
|
||||
// spelled with the same Qt type names methods use, so a field can name another
|
||||
// record ("Status"), a list of them ("QList<Status>") or a map of them
|
||||
// ("QMap<QString, Status>"). The struct is nested in the wrapper class —
|
||||
// `InfoModule::Status` — because one module consuming two deps that each
|
||||
// declare `Status` includes both wrappers into the same translation unit.
|
||||
//
|
||||
// An empty record set leaves every emission path byte-for-byte as it was.
|
||||
|
||||
struct RecordField { QString name; QString type; };
|
||||
struct RecordDef { QString name; QVector<RecordField> fields; };
|
||||
using RecordSet = QVector<RecordDef>;
|
||||
|
||||
// Forward declarations: the Lp (Qt-free) conversion helpers live further down
|
||||
// with the rest of the Lp backend, but the record helpers below dispatch to
|
||||
// them for non-record field types.
|
||||
static QString lpPushExpr(const QString& qtType, const QString& argName);
|
||||
static QString lpFromJsonExpr(const QString& qtType, const QString& jv);
|
||||
|
||||
static RecordSet parseRecords(const QJsonArray& records)
|
||||
{
|
||||
RecordSet out;
|
||||
for (const QJsonValue& rv : records) {
|
||||
const QJsonObject ro = rv.toObject();
|
||||
RecordDef def;
|
||||
def.name = ro.value("name").toString();
|
||||
if (def.name.isEmpty()) continue;
|
||||
for (const QJsonValue& fv : ro.value("fields").toArray()) {
|
||||
const QJsonObject fo = fv.toObject();
|
||||
RecordField f;
|
||||
f.name = fo.value("name").toString();
|
||||
f.type = fo.value("type").toString();
|
||||
if (f.name.isEmpty()) continue;
|
||||
def.fields.append(f);
|
||||
}
|
||||
out.append(def);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
static bool isRecordName(const RecordSet& rs, const QString& name)
|
||||
{
|
||||
for (const RecordDef& d : rs) if (d.name == name) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// How a type name mentions a record, if at all.
|
||||
enum class RecordShape { None, Scalar, List, Map };
|
||||
|
||||
static RecordShape recordShape(const RecordSet& rs, const QString& t, QString* elem)
|
||||
{
|
||||
if (rs.isEmpty()) return RecordShape::None;
|
||||
if (isRecordName(rs, t)) { if (elem) *elem = t; return RecordShape::Scalar; }
|
||||
if (t.startsWith("QList<") && t.endsWith(">")) {
|
||||
const QString e = t.mid(6, t.size() - 7).trimmed();
|
||||
if (isRecordName(rs, e)) { if (elem) *elem = e; return RecordShape::List; }
|
||||
}
|
||||
if (t.startsWith("QMap<QString,") && t.endsWith(">")) {
|
||||
const QString e = t.mid(13, t.size() - 14).trimmed();
|
||||
if (isRecordName(rs, e)) { if (elem) *elem = e; return RecordShape::Map; }
|
||||
}
|
||||
return RecordShape::None;
|
||||
}
|
||||
|
||||
// The C++ spelling of a record-bearing type, or empty when `t` names none.
|
||||
// `qual` qualifies the nested struct ("InfoModule::") where class scope does
|
||||
// not already apply — i.e. a return type written before the `Class::` in a
|
||||
// definition.
|
||||
static QString recordCppType(const RecordSet& rs, const QString& t, ApiStyle style, const QString& qual)
|
||||
{
|
||||
QString elem;
|
||||
const RecordShape shape = recordShape(rs, t, &elem);
|
||||
const QString q = qual + elem;
|
||||
switch (shape) {
|
||||
case RecordShape::None: return QString();
|
||||
case RecordShape::Scalar: return q;
|
||||
case RecordShape::List:
|
||||
return style == ApiStyle::Qt ? "QList<" + q + ">" : "std::vector<" + q + ">";
|
||||
case RecordShape::Map:
|
||||
return style == ApiStyle::Qt ? "QMap<QString, " + q + ">"
|
||||
: "std::map<std::string, " + q + ">";
|
||||
}
|
||||
return QString();
|
||||
}
|
||||
|
||||
// File-local conversion helpers emitted into the generated .cpp — never into
|
||||
// the header, so a std/lp consumer's own translation units stay free of the
|
||||
// wire type (QVariant / nlohmann::json) the conversion is written in.
|
||||
static QString recToWireFn(const QString& record) { return "recToWire_" + record; }
|
||||
static QString recFromWireFn(const QString& record) { return "recFromWire_" + record; }
|
||||
|
||||
// Record value -> wire value, and back. Empty when `t` names no record.
|
||||
static QString recordToWireExpr(const RecordSet& rs, const QString& t, ApiStyle style, const QString& expr)
|
||||
{
|
||||
QString elem;
|
||||
const RecordShape shape = recordShape(rs, t, &elem);
|
||||
if (shape == RecordShape::None) return QString();
|
||||
const QString conv = recToWireFn(elem);
|
||||
if (shape == RecordShape::Scalar) return conv + "(" + expr + ")";
|
||||
// Locals are named apart from the record encoder/decoder's own `__m` / `__j`
|
||||
// / `__out`: these lambdas are emitted INSIDE those functions when a record
|
||||
// has a container-of-record field, and a shadowing local silently reads
|
||||
// itself (caught by -Wuninitialized, not by any assertion on the text).
|
||||
if (style == ApiStyle::Lp) {
|
||||
if (shape == RecordShape::List)
|
||||
return "[&]{ nlohmann::json __acc = nlohmann::json::array(); for (const auto& __e : "
|
||||
+ expr + ") __acc.push_back(" + conv + "(__e)); return __acc; }()";
|
||||
return "[&]{ nlohmann::json __acc = nlohmann::json::object(); for (const auto& __kv : "
|
||||
+ expr + ") __acc[__kv.first] = " + conv + "(__kv.second); return __acc; }()";
|
||||
}
|
||||
if (shape == RecordShape::List)
|
||||
return "[&]{ QVariantList __acc; for (const auto& __e : " + expr
|
||||
+ ") __acc.append(" + conv + "(__e)); return __acc; }()";
|
||||
// Qt keys are QString, std keys std::string.
|
||||
if (style == ApiStyle::Qt)
|
||||
return "[&]{ QVariantMap __acc; for (auto __i = " + expr + ".cbegin(); __i != " + expr
|
||||
+ ".cend(); ++__i) __acc.insert(__i.key(), " + conv + "(__i.value())); return __acc; }()";
|
||||
return "[&]{ QVariantMap __acc; for (const auto& __kv : " + expr
|
||||
+ ") __acc.insert(QString::fromStdString(__kv.first), " + conv + "(__kv.second)); return __acc; }()";
|
||||
}
|
||||
|
||||
static QString recordFromWireExpr(const RecordSet& rs, const QString& t, ApiStyle style,
|
||||
const QString& wire, const QString& qual)
|
||||
{
|
||||
QString elem;
|
||||
const RecordShape shape = recordShape(rs, t, &elem);
|
||||
if (shape == RecordShape::None) return QString();
|
||||
const QString conv = recFromWireFn(elem);
|
||||
const QString cpp = recordCppType(rs, t, style, qual);
|
||||
if (shape == RecordShape::Scalar) return conv + "(" + wire + ")";
|
||||
if (style == ApiStyle::Lp) {
|
||||
if (shape == RecordShape::List)
|
||||
return "[&]{ " + cpp + " __acc; const nlohmann::json& __src = " + wire
|
||||
+ "; if (__src.is_array()) for (const auto& __e : __src) __acc.push_back(" + conv
|
||||
+ "(__e)); return __acc; }()";
|
||||
return "[&]{ " + cpp + " __acc; const nlohmann::json& __src = " + wire
|
||||
+ "; if (__src.is_object()) for (auto __i = __src.begin(); __i != __src.end(); ++__i) "
|
||||
"__acc[__i.key()] = " + conv + "(__i.value()); return __acc; }()";
|
||||
}
|
||||
if (shape == RecordShape::List)
|
||||
return "[&]{ " + cpp + " __acc; for (const QVariant& __e : (" + wire
|
||||
+ ").toList()) __acc.push_back(" + conv + "(__e)); return __acc; }()";
|
||||
if (style == ApiStyle::Qt)
|
||||
return "[&]{ " + cpp + " __acc; const QVariantMap __src = (" + wire
|
||||
+ ").toMap(); for (auto __i = __src.cbegin(); __i != __src.cend(); ++__i) "
|
||||
"__acc.insert(__i.key(), " + conv + "(__i.value())); return __acc; }()";
|
||||
return "[&]{ " + cpp + " __acc; const QVariantMap __src = (" + wire
|
||||
+ ").toMap(); for (auto __i = __src.cbegin(); __i != __src.cend(); ++__i) "
|
||||
"__acc[__i.key().toStdString()] = " + conv + "(__i.value()); return __acc; }()";
|
||||
}
|
||||
|
||||
// Param-type predicate: passed by const-ref?
|
||||
static bool isStdRefType(const QString& t)
|
||||
{
|
||||
return t == "std::string" || t.startsWith("std::vector")
|
||||
|| t == "std::map" || t.startsWith("std::map")
|
||||
|| t == "LogosMap" || t == "LogosList";
|
||||
}
|
||||
|
||||
@@ -214,10 +375,144 @@ static bool isQtRefType(const QString& t)
|
||||
|| t == "QJsonArray" || t == "QVariantList" || t == "QVariantMap";
|
||||
}
|
||||
|
||||
QString makeHeader(const QString& moduleName, const QString& className, const QJsonArray& methods, ApiStyle apiStyle, const QJsonArray& events, BindMode bindMode)
|
||||
// ─── Record-aware type / conversion dispatch ─────────────────────────────
|
||||
//
|
||||
// The one entry point every emission site goes through. A record-bearing type
|
||||
// takes the record path; everything else falls through to the pre-existing
|
||||
// mapping tables unchanged, so an empty record set is a no-op.
|
||||
|
||||
static QString paramTypeFor(const QString& qtType, ApiStyle style, const RecordSet& rs,
|
||||
const QString& qual = QString())
|
||||
{
|
||||
const QString rec = recordCppType(rs, qtType, style, qual);
|
||||
if (!rec.isEmpty()) return rec;
|
||||
return (style == ApiStyle::Qt) ? mapParamType(qtType) : mapParamTypeStd(qtType);
|
||||
}
|
||||
|
||||
static QString returnTypeFor(const QString& qtType, ApiStyle style, const RecordSet& rs,
|
||||
const QString& qual = QString())
|
||||
{
|
||||
const QString rec = recordCppType(rs, qtType, style, qual);
|
||||
if (!rec.isEmpty()) return rec;
|
||||
return (style == ApiStyle::Qt) ? mapReturnType(qtType) : mapReturnTypeStd(qtType);
|
||||
}
|
||||
|
||||
// Records are structs — always by const-ref, never copied into a call.
|
||||
static bool byRefFor(const QString& qtType, const QString& cppType, ApiStyle style, const RecordSet& rs)
|
||||
{
|
||||
if (recordShape(rs, qtType, nullptr) != RecordShape::None) return true;
|
||||
return (style == ApiStyle::Qt) ? isQtRefType(cppType) : isStdRefType(cppType);
|
||||
}
|
||||
|
||||
// Typed value -> wire value (QVariant for Qt/Std, nlohmann::json for Lp).
|
||||
static QString toWireFor(const QString& qtType, ApiStyle style, const RecordSet& rs, const QString& expr)
|
||||
{
|
||||
const QString rec = recordToWireExpr(rs, qtType, style, expr);
|
||||
if (!rec.isEmpty()) return rec;
|
||||
if (style == ApiStyle::Lp) return lpPushExpr(qtType, expr);
|
||||
if (style == ApiStyle::Std) return stdParamToQVariant(qtType, expr);
|
||||
return expr; // Qt: the wrapper's own surface already IS the wire type
|
||||
}
|
||||
|
||||
// Wire value -> typed value.
|
||||
static QString fromWireFor(const QString& qtType, ApiStyle style, const RecordSet& rs,
|
||||
const QString& wire, const QString& qual = QString())
|
||||
{
|
||||
const QString rec = recordFromWireExpr(rs, qtType, style, wire, qual);
|
||||
if (!rec.isEmpty()) return rec;
|
||||
if (style == ApiStyle::Lp) return lpFromJsonExpr(qtType, wire);
|
||||
if (style == ApiStyle::Std) return qVariantToStdReturn(qtType, wire);
|
||||
return toQVariantConversion(mapParamType(qtType), wire);
|
||||
}
|
||||
|
||||
// The struct declarations, emitted inside the wrapper class.
|
||||
static void emitRecordStructs(QTextStream& s, const RecordSet& rs, ApiStyle style)
|
||||
{
|
||||
if (rs.isEmpty()) return;
|
||||
s << " // Record types declared by the contract.\n";
|
||||
for (const RecordDef& d : rs) {
|
||||
s << " struct " << d.name << " {\n";
|
||||
for (const RecordField& f : d.fields)
|
||||
s << " " << paramTypeFor(f.type, style, rs) << " " << f.name << "{};\n";
|
||||
s << " };\n";
|
||||
}
|
||||
s << "\n";
|
||||
}
|
||||
|
||||
// The struct <-> wire conversions, emitted as file-local statics in the
|
||||
// generated .cpp. Declared up front so records can reference each other (and
|
||||
// themselves, through a list field) regardless of declaration order.
|
||||
static void emitRecordConversions(QTextStream& s, const RecordSet& rs, ApiStyle style,
|
||||
const QString& className)
|
||||
{
|
||||
if (rs.isEmpty()) return;
|
||||
const QString wire = (style == ApiStyle::Lp) ? "nlohmann::json" : "QVariant";
|
||||
const QString qual = className + "::";
|
||||
|
||||
for (const RecordDef& d : rs) {
|
||||
s << "static " << wire << " " << recToWireFn(d.name)
|
||||
<< "(const " << qual << d.name << "& v);\n";
|
||||
s << "static " << qual << d.name << " " << recFromWireFn(d.name)
|
||||
<< "(const " << wire << "& w);\n";
|
||||
}
|
||||
s << "\n";
|
||||
|
||||
for (const RecordDef& d : rs) {
|
||||
// Encode.
|
||||
s << "static " << wire << " " << recToWireFn(d.name)
|
||||
<< "(const " << qual << d.name << "& v) {\n";
|
||||
if (style == ApiStyle::Lp) {
|
||||
s << " nlohmann::json __j = nlohmann::json::object();\n";
|
||||
for (const RecordField& f : d.fields)
|
||||
s << " __j[\"" << f.name << "\"] = "
|
||||
<< toWireFor(f.type, style, rs, "v." + f.name) << ";\n";
|
||||
s << " return __j;\n";
|
||||
} else {
|
||||
s << " QVariantMap __m;\n";
|
||||
for (const RecordField& f : d.fields) {
|
||||
// Qt's surface type IS the wire type for non-record fields, so
|
||||
// fromValue is what puts it in the map; records/containers
|
||||
// already produce a QVariant-compatible value.
|
||||
const QString v = toWireFor(f.type, style, rs, "v." + f.name);
|
||||
const bool isRec = recordShape(rs, f.type, nullptr) != RecordShape::None;
|
||||
s << " __m.insert(QStringLiteral(\"" << f.name << "\"), "
|
||||
<< (isRec || style == ApiStyle::Std ? v : "QVariant::fromValue(" + v + ")")
|
||||
<< ");\n";
|
||||
}
|
||||
s << " return __m;\n";
|
||||
}
|
||||
s << "}\n\n";
|
||||
|
||||
// Decode. A missing / mistyped field keeps its default rather than
|
||||
// failing the whole call — same leniency the scalar paths use.
|
||||
s << "static " << qual << d.name << " " << recFromWireFn(d.name)
|
||||
<< "(const " << wire << "& w) {\n";
|
||||
s << " " << qual << d.name << " __out;\n";
|
||||
if (style == ApiStyle::Lp) {
|
||||
s << " if (!w.is_object()) return __out;\n";
|
||||
for (const RecordField& f : d.fields) {
|
||||
const QString acc = "w.at(\"" + f.name + "\")";
|
||||
s << " if (w.contains(\"" << f.name << "\")) __out." << f.name << " = "
|
||||
<< fromWireFor(f.type, style, rs, acc, qual) << ";\n";
|
||||
}
|
||||
} else {
|
||||
s << " const QVariantMap __m = w.toMap();\n";
|
||||
for (const RecordField& f : d.fields) {
|
||||
const QString acc = "__m.value(QStringLiteral(\"" + f.name + "\"))";
|
||||
s << " __out." << f.name << " = "
|
||||
<< fromWireFor(f.type, style, rs, acc, qual) << ";\n";
|
||||
}
|
||||
}
|
||||
s << " return __out;\n";
|
||||
s << "}\n\n";
|
||||
}
|
||||
}
|
||||
|
||||
QString makeHeader(const QString& moduleName, const QString& className, const QJsonArray& methods, ApiStyle apiStyle, const QJsonArray& events, BindMode bindMode, const QJsonArray& records)
|
||||
{
|
||||
if (apiStyle == ApiStyle::Lp)
|
||||
return makeHeaderLp(moduleName, className, methods, events, bindMode);
|
||||
return makeHeaderLp(moduleName, className, methods, events, bindMode, records);
|
||||
const RecordSet rs = parseRecords(records);
|
||||
QString h;
|
||||
QTextStream s(&h);
|
||||
s << "#pragma once\n";
|
||||
@@ -240,6 +535,8 @@ QString makeHeader(const QString& moduleName, const QString& className, const QJ
|
||||
// any events. Cheap to include unconditionally — keeps the
|
||||
// header symmetric with the Qt-style branch.
|
||||
if (!events.isEmpty()) s << "#include \"logos_object.h\"\n";
|
||||
// Record maps are std::map on the std surface.
|
||||
if (!rs.isEmpty()) s << "#include <map>\n";
|
||||
s << "\n";
|
||||
} else {
|
||||
s << "#include <QString>\n";
|
||||
@@ -258,6 +555,7 @@ QString makeHeader(const QString& moduleName, const QString& className, const QJ
|
||||
}
|
||||
s << "class " << className << " {\n";
|
||||
s << "public:\n";
|
||||
emitRecordStructs(s, rs, apiStyle);
|
||||
if (bindMode == BindMode::Bound) {
|
||||
// Interface wrapper: the module to talk to is chosen at runtime.
|
||||
s << " explicit " << className << "(LogosAPI* api, const QString& moduleName);\n\n";
|
||||
@@ -308,9 +606,8 @@ QString makeHeader(const QString& moduleName, const QString& className, const QJ
|
||||
for (int i = 0; i < evParams.size(); ++i) {
|
||||
const QJsonObject p = evParams.at(i).toObject();
|
||||
QString qtPt = p.value("type").toString();
|
||||
QString pt = (apiStyle == ApiStyle::Std)
|
||||
? mapParamTypeStd(qtPt) : mapParamType(qtPt);
|
||||
bool byRef = (apiStyle == ApiStyle::Std) ? isStdRefType(pt) : isQtRefType(pt);
|
||||
QString pt = paramTypeFor(qtPt, apiStyle, rs);
|
||||
bool byRef = byRefFor(qtPt, pt, apiStyle, rs);
|
||||
if (byRef) cbParams += "const " + pt + "& ";
|
||||
else cbParams += pt + " ";
|
||||
cbParams += p.value("name").toString();
|
||||
@@ -327,17 +624,15 @@ QString makeHeader(const QString& moduleName, const QString& className, const QJ
|
||||
if (!invokable) continue;
|
||||
const QString name = o.value("name").toString();
|
||||
const QString qtRet = o.value("returnType").toString();
|
||||
const QString ret = (apiStyle == ApiStyle::Std)
|
||||
? mapReturnTypeStd(qtRet) : mapReturnType(qtRet);
|
||||
const QString ret = returnTypeFor(qtRet, apiStyle, rs);
|
||||
s << " " << ret << " " << name << "(";
|
||||
QJsonArray params = o.value("parameters").toArray();
|
||||
for (int i = 0; i < params.size(); ++i) {
|
||||
QJsonObject p = params.at(i).toObject();
|
||||
QString qtPt = p.value("type").toString();
|
||||
QString pt = (apiStyle == ApiStyle::Std)
|
||||
? mapParamTypeStd(qtPt) : mapParamType(qtPt);
|
||||
QString pt = paramTypeFor(qtPt, apiStyle, rs);
|
||||
QString pn = p.value("name").toString();
|
||||
bool byRef = (apiStyle == ApiStyle::Std) ? isStdRefType(pt) : isQtRefType(pt);
|
||||
bool byRef = byRefFor(qtPt, pt, apiStyle, rs);
|
||||
if (byRef) s << "const " << pt << "& " << pn;
|
||||
else s << pt << " " << pn;
|
||||
if (i + 1 < params.size()) s << ", ";
|
||||
@@ -355,10 +650,9 @@ QString makeHeader(const QString& moduleName, const QString& className, const QJ
|
||||
for (int i = 0; i < params.size(); ++i) {
|
||||
QJsonObject p = params.at(i).toObject();
|
||||
QString qtPt = p.value("type").toString();
|
||||
QString pt = (apiStyle == ApiStyle::Std)
|
||||
? mapParamTypeStd(qtPt) : mapParamType(qtPt);
|
||||
QString pt = paramTypeFor(qtPt, apiStyle, rs);
|
||||
QString pn = p.value("name").toString();
|
||||
bool byRef = (apiStyle == ApiStyle::Std) ? isStdRefType(pt) : isQtRefType(pt);
|
||||
bool byRef = byRefFor(qtPt, pt, apiStyle, rs);
|
||||
if (byRef) s << "const " << pt << "& " << pn;
|
||||
else s << pt << " " << pn;
|
||||
if (i + 1 < params.size()) s << ", ";
|
||||
@@ -399,10 +693,11 @@ QString makeHeader(const QString& moduleName, const QString& className, const QJ
|
||||
return h;
|
||||
}
|
||||
|
||||
QString makeSource(const QString& moduleName, const QString& className, const QString& headerBaseName, const QJsonArray& methods, ApiStyle apiStyle, const QJsonArray& events, BindMode bindMode)
|
||||
QString makeSource(const QString& moduleName, const QString& className, const QString& headerBaseName, const QJsonArray& methods, ApiStyle apiStyle, const QJsonArray& events, BindMode bindMode, const QJsonArray& records)
|
||||
{
|
||||
if (apiStyle == ApiStyle::Lp)
|
||||
return makeSourceLp(moduleName, className, headerBaseName, methods, events, bindMode);
|
||||
return makeSourceLp(moduleName, className, headerBaseName, methods, events, bindMode, records);
|
||||
const RecordSet rs = parseRecords(records);
|
||||
QString c;
|
||||
QTextStream s(&c);
|
||||
s << "#include \"" << headerBaseName << "\"\n\n";
|
||||
@@ -423,7 +718,12 @@ QString makeSource(const QString& moduleName, const QString& className, const QS
|
||||
// std mode when events are present.
|
||||
if (!events.isEmpty()) s << "#include \"logos_object.h\"\n";
|
||||
}
|
||||
if (apiStyle == ApiStyle::Qt && !rs.isEmpty()) {
|
||||
// Record conversions build QVariantMaps regardless of api style.
|
||||
s << "#include <QVariantMap>\n";
|
||||
}
|
||||
s << "\n";
|
||||
emitRecordConversions(s, rs, apiStyle, className);
|
||||
// The expression every remote call uses to name its target module.
|
||||
// Static: the baked string literal "<moduleName>" (unchanged
|
||||
// behaviour). Bound: the m_moduleName member set from the runtime ctor
|
||||
@@ -535,9 +835,8 @@ QString makeSource(const QString& moduleName, const QString& className, const QS
|
||||
for (int i = 0; i < evParams.size(); ++i) {
|
||||
const QJsonObject p = evParams.at(i).toObject();
|
||||
QString qtPt = p.value("type").toString();
|
||||
QString pt = (apiStyle == ApiStyle::Std)
|
||||
? mapParamTypeStd(qtPt) : mapParamType(qtPt);
|
||||
bool byRef = (apiStyle == ApiStyle::Std) ? isStdRefType(pt) : isQtRefType(pt);
|
||||
QString pt = paramTypeFor(qtPt, apiStyle, rs);
|
||||
bool byRef = byRefFor(qtPt, pt, apiStyle, rs);
|
||||
if (byRef) cbParams += "const " + pt + "& ";
|
||||
else cbParams += pt + " ";
|
||||
cbParams += p.value("name").toString();
|
||||
@@ -561,13 +860,7 @@ QString makeSource(const QString& moduleName, const QString& className, const QS
|
||||
QString qtPt = p.value("type").toString();
|
||||
// Build the QVariant → typed-arg conversion expression.
|
||||
const QString argExpr = QString("_args.at(%1)").arg(i);
|
||||
QString conv;
|
||||
if (apiStyle == ApiStyle::Std) {
|
||||
conv = qVariantToStdReturn(qtPt, argExpr);
|
||||
} else {
|
||||
conv = toQVariantConversion(mapParamType(qtPt), argExpr);
|
||||
}
|
||||
s << conv;
|
||||
s << fromWireFor(qtPt, apiStyle, rs, argExpr);
|
||||
if (i + 1 < evParams.size()) s << ", ";
|
||||
}
|
||||
s << ");\n";
|
||||
@@ -582,8 +875,11 @@ QString makeSource(const QString& moduleName, const QString& className, const QS
|
||||
if (!invokable) continue;
|
||||
const QString name = o.value("name").toString();
|
||||
const QString qtRet = o.value("returnType").toString();
|
||||
const QString ret = (apiStyle == ApiStyle::Std)
|
||||
? mapReturnTypeStd(qtRet) : mapReturnType(qtRet);
|
||||
// Inside the class's own scope (parameter lists, bodies) a nested
|
||||
// record needs no qualification; a return type written before the
|
||||
// `Class::` in a definition does.
|
||||
const QString ret = returnTypeFor(qtRet, apiStyle, rs);
|
||||
const QString retQual = returnTypeFor(qtRet, apiStyle, rs, className + "::");
|
||||
QJsonArray params = o.value("parameters").toArray();
|
||||
|
||||
// Helper closures kept inline so the two branches don't get
|
||||
@@ -591,21 +887,20 @@ QString makeSource(const QString& moduleName, const QString& className, const QS
|
||||
// the only thing that varies between Qt and Std modes.
|
||||
auto emitParam = [&](const QJsonObject& p, bool& byRefOut) {
|
||||
QString qtPt = p.value("type").toString();
|
||||
QString pt = (apiStyle == ApiStyle::Std)
|
||||
? mapParamTypeStd(qtPt) : mapParamType(qtPt);
|
||||
QString pt = paramTypeFor(qtPt, apiStyle, rs);
|
||||
QString pn = p.value("name").toString();
|
||||
byRefOut = (apiStyle == ApiStyle::Std) ? isStdRefType(pt) : isQtRefType(pt);
|
||||
byRefOut = byRefFor(qtPt, pt, apiStyle, rs);
|
||||
if (byRefOut) s << "const " << pt << "& " << pn;
|
||||
else s << pt << " " << pn;
|
||||
};
|
||||
auto wireArg = [&](const QJsonObject& p) -> QString {
|
||||
QString qtPt = p.value("type").toString();
|
||||
QString pn = p.value("name").toString();
|
||||
return (apiStyle == ApiStyle::Std) ? stdParamToQVariant(qtPt, pn) : pn;
|
||||
return toWireFor(qtPt, apiStyle, rs, pn);
|
||||
};
|
||||
|
||||
// Signature
|
||||
s << ret << " " << className << "::" << name << "(";
|
||||
s << retQual << " " << className << "::" << name << "(";
|
||||
for (int i = 0; i < params.size(); ++i) {
|
||||
bool byRef;
|
||||
emitParam(params.at(i).toObject(), byRef);
|
||||
@@ -641,8 +936,11 @@ QString makeSource(const QString& moduleName, const QString& className, const QS
|
||||
<< ": remote call failed:\" << QString::fromStdString(_err.message);\n";
|
||||
|
||||
// Return conversion
|
||||
const bool retIsRecord = recordShape(rs, qtRet, nullptr) != RecordShape::None;
|
||||
if (ret == "void") {
|
||||
// nothing
|
||||
} else if (retIsRecord) {
|
||||
s << " return " << fromWireFor(qtRet, apiStyle, rs, "_result") << ";\n";
|
||||
} else if (apiStyle == ApiStyle::Std) {
|
||||
s << " return " << qVariantToStdReturn(qtRet, "_result") << ";\n";
|
||||
} else if (ret == "bool") {
|
||||
@@ -705,6 +1003,10 @@ QString makeSource(const QString& moduleName, const QString& className, const QS
|
||||
s << ", [callback](QVariant v) {\n";
|
||||
if (ret == "void") {
|
||||
s << " (void)v; callback();\n";
|
||||
} else if (retIsRecord) {
|
||||
// A record decodes field by field; an invalid QVariant yields a
|
||||
// default-constructed struct, matching the scalar paths.
|
||||
s << " callback(" << fromWireFor(qtRet, apiStyle, rs, "v", className + "::") << ");\n";
|
||||
} else if (apiStyle == ApiStyle::Std) {
|
||||
// Default-construct on dispatch failure, matching the
|
||||
// existing Qt code path which falls back to a zero / empty
|
||||
@@ -887,6 +1189,7 @@ static QString lpFromJsonExpr(const QString& qtType, const QString& jv)
|
||||
if (t == "void") return QString();
|
||||
if (t == "std::string") return "(" + jv + ".is_string() ? " + jv + ".get<std::string>() : std::string())";
|
||||
if (t == "int64_t") return "(" + jv + ".is_number_integer() ? " + jv + ".get<int64_t>() : (" + jv + ".is_number() ? static_cast<int64_t>(" + jv + ".get<double>()) : (int64_t)0))";
|
||||
if (t == "uint64_t") return "(" + jv + ".is_number_integer() ? " + jv + ".get<uint64_t>() : (" + jv + ".is_number() ? static_cast<uint64_t>(" + jv + ".get<double>()) : (uint64_t)0))";
|
||||
if (t == "double") return "(" + jv + ".is_number() ? " + jv + ".get<double>() : 0.0)";
|
||||
if (t == "bool") return "(" + jv + ".is_boolean() ? " + jv + ".get<bool>() : false)";
|
||||
if (t == "std::vector<std::string>") return "logos::jsonToStringVec(" + jv + ")";
|
||||
@@ -905,14 +1208,15 @@ static QString lpFromJsonExpr(const QString& qtType, const QString& jv)
|
||||
|
||||
// Build the callback parameter list (std types, by-ref where appropriate) for
|
||||
// a typed event accessor `on<Event>`.
|
||||
static QString lpEventCbParams(const QJsonArray& evParams)
|
||||
static QString lpEventCbParams(const QJsonArray& evParams, const RecordSet& rs)
|
||||
{
|
||||
QString cbParams;
|
||||
for (int i = 0; i < evParams.size(); ++i) {
|
||||
const QJsonObject p = evParams.at(i).toObject();
|
||||
const QString pt = mapParamTypeStd(p.value("type").toString());
|
||||
if (isStdRefType(pt)) cbParams += "const " + pt + "& ";
|
||||
else cbParams += pt + " ";
|
||||
const QString qtPt = p.value("type").toString();
|
||||
const QString pt = paramTypeFor(qtPt, ApiStyle::Lp, rs);
|
||||
if (byRefFor(qtPt, pt, ApiStyle::Lp, rs)) cbParams += "const " + pt + "& ";
|
||||
else cbParams += pt + " ";
|
||||
cbParams += p.value("name").toString();
|
||||
if (i + 1 < evParams.size()) cbParams += ", ";
|
||||
}
|
||||
@@ -926,9 +1230,10 @@ static QString lpEventAccessorName(const QString& evName)
|
||||
return QString("on") + cap;
|
||||
}
|
||||
|
||||
QString makeHeaderLp(const QString& moduleName, const QString& className, const QJsonArray& methods, const QJsonArray& events, BindMode bindMode)
|
||||
QString makeHeaderLp(const QString& moduleName, const QString& className, const QJsonArray& methods, const QJsonArray& events, BindMode bindMode, const QJsonArray& records)
|
||||
{
|
||||
(void)moduleName;
|
||||
const RecordSet rs = parseRecords(records);
|
||||
QString h;
|
||||
QTextStream s(&h);
|
||||
s << "#pragma once\n";
|
||||
@@ -940,10 +1245,14 @@ QString makeHeaderLp(const QString& moduleName, const QString& className, const
|
||||
s << "#include \"logos_json.h\"\n";
|
||||
s << "#include \"logos_result.h\"\n";
|
||||
s << "#include \"logos_call_error.h\"\n";
|
||||
s << "#include \"logos_lp_client.h\"\n\n";
|
||||
s << "#include \"logos_lp_client.h\"\n";
|
||||
// Record maps are std::map on the Qt-free surface.
|
||||
if (!rs.isEmpty()) s << "#include <map>\n";
|
||||
s << "\n";
|
||||
|
||||
s << "class " << className << " {\n";
|
||||
s << "public:\n";
|
||||
emitRecordStructs(s, rs, ApiStyle::Lp);
|
||||
if (bindMode == BindMode::Bound) {
|
||||
// Bound (interface) wrappers are THIN, copyable handles over
|
||||
// umbrella-owned persistent State, so a transient
|
||||
@@ -969,7 +1278,7 @@ QString makeHeaderLp(const QString& moduleName, const QString& className, const
|
||||
const QString evName = eo.value("name").toString();
|
||||
if (evName.isEmpty()) continue;
|
||||
s << " bool " << lpEventAccessorName(evName)
|
||||
<< "(std::function<void(" << lpEventCbParams(eo.value("params").toArray()) << ")> callback);\n";
|
||||
<< "(std::function<void(" << lpEventCbParams(eo.value("params").toArray(), rs) << ")> callback);\n";
|
||||
}
|
||||
if (!events.isEmpty()) s << "\n";
|
||||
|
||||
@@ -978,15 +1287,16 @@ QString makeHeaderLp(const QString& moduleName, const QString& className, const
|
||||
const QJsonObject o = v.toObject();
|
||||
if (!o.value("isInvokable").toBool()) continue;
|
||||
const QString name = o.value("name").toString();
|
||||
const QString ret = mapReturnTypeStd(o.value("returnType").toString());
|
||||
const QString ret = returnTypeFor(o.value("returnType").toString(), ApiStyle::Lp, rs);
|
||||
const QJsonArray params = o.value("parameters").toArray();
|
||||
|
||||
s << " " << ret << " " << name << "(";
|
||||
for (int i = 0; i < params.size(); ++i) {
|
||||
const QJsonObject p = params.at(i).toObject();
|
||||
const QString pt = mapParamTypeStd(p.value("type").toString());
|
||||
if (isStdRefType(pt)) s << "const " << pt << "& " << p.value("name").toString();
|
||||
else s << pt << " " << p.value("name").toString();
|
||||
const QString qtPt = p.value("type").toString();
|
||||
const QString pt = paramTypeFor(qtPt, ApiStyle::Lp, rs);
|
||||
if (byRefFor(qtPt, pt, ApiStyle::Lp, rs)) s << "const " << pt << "& " << p.value("name").toString();
|
||||
else s << pt << " " << p.value("name").toString();
|
||||
if (i + 1 < params.size()) s << ", ";
|
||||
}
|
||||
if (!params.isEmpty()) s << ", ";
|
||||
@@ -998,9 +1308,10 @@ QString makeHeaderLp(const QString& moduleName, const QString& className, const
|
||||
s << " void " << name << "Async(";
|
||||
for (int i = 0; i < params.size(); ++i) {
|
||||
const QJsonObject p = params.at(i).toObject();
|
||||
const QString pt = mapParamTypeStd(p.value("type").toString());
|
||||
if (isStdRefType(pt)) s << "const " << pt << "& " << p.value("name").toString();
|
||||
else s << pt << " " << p.value("name").toString();
|
||||
const QString qtPt = p.value("type").toString();
|
||||
const QString pt = paramTypeFor(qtPt, ApiStyle::Lp, rs);
|
||||
if (byRefFor(qtPt, pt, ApiStyle::Lp, rs)) s << "const " << pt << "& " << p.value("name").toString();
|
||||
else s << pt << " " << p.value("name").toString();
|
||||
if (i + 1 < params.size()) s << ", ";
|
||||
}
|
||||
if (!params.isEmpty()) s << ", ";
|
||||
@@ -1018,12 +1329,14 @@ QString makeHeaderLp(const QString& moduleName, const QString& className, const
|
||||
return h;
|
||||
}
|
||||
|
||||
QString makeSourceLp(const QString& moduleName, const QString& className, const QString& headerBaseName, const QJsonArray& methods, const QJsonArray& events, BindMode bindMode)
|
||||
QString makeSourceLp(const QString& moduleName, const QString& className, const QString& headerBaseName, const QJsonArray& methods, const QJsonArray& events, BindMode bindMode, const QJsonArray& records)
|
||||
{
|
||||
const RecordSet rs = parseRecords(records);
|
||||
QString c;
|
||||
QTextStream s(&c);
|
||||
s << "#include \"" << headerBaseName << "\"\n";
|
||||
s << "#include <nlohmann/json.hpp>\n\n";
|
||||
emitRecordConversions(s, rs, ApiStyle::Lp, className);
|
||||
|
||||
// How the wrapper reaches its persistent LpClient + subscription store.
|
||||
// Static (concrete dep): owns them by value — the wrapper itself is a
|
||||
@@ -1047,14 +1360,15 @@ QString makeSourceLp(const QString& moduleName, const QString& className, const
|
||||
if (evName.isEmpty()) continue;
|
||||
const QJsonArray evParams = eo.value("params").toArray();
|
||||
s << "bool " << className << "::" << lpEventAccessorName(evName)
|
||||
<< "(std::function<void(" << lpEventCbParams(evParams) << ")> callback) {\n";
|
||||
<< "(std::function<void(" << lpEventCbParams(evParams, rs) << ")> callback) {\n";
|
||||
s << " if (!callback) return false;\n";
|
||||
s << " auto _sub = " << clientExpr << ".subscribe(\"" << evName << "\", [callback](nlohmann::json _a) {\n";
|
||||
s << " if (!_a.is_array() || _a.size() < " << evParams.size() << ") return;\n";
|
||||
s << " callback(";
|
||||
for (int i = 0; i < evParams.size(); ++i) {
|
||||
const QJsonObject p = evParams.at(i).toObject();
|
||||
s << lpFromJsonExpr(p.value("type").toString(), QString("_a.at(%1)").arg(i));
|
||||
s << fromWireFor(p.value("type").toString(), ApiStyle::Lp, rs,
|
||||
QString("_a.at(%1)").arg(i), className + "::");
|
||||
if (i + 1 < evParams.size()) s << ", ";
|
||||
}
|
||||
s << ");\n";
|
||||
@@ -1071,15 +1385,17 @@ QString makeSourceLp(const QString& moduleName, const QString& className, const
|
||||
if (!o.value("isInvokable").toBool()) continue;
|
||||
const QString name = o.value("name").toString();
|
||||
const QString qtRet = o.value("returnType").toString();
|
||||
const QString ret = mapReturnTypeStd(qtRet);
|
||||
const QString ret = returnTypeFor(qtRet, ApiStyle::Lp, rs);
|
||||
const QString retQual = returnTypeFor(qtRet, ApiStyle::Lp, rs, className + "::");
|
||||
const QJsonArray params = o.value("parameters").toArray();
|
||||
|
||||
auto emitParams = [&]() {
|
||||
for (int i = 0; i < params.size(); ++i) {
|
||||
const QJsonObject p = params.at(i).toObject();
|
||||
const QString pt = mapParamTypeStd(p.value("type").toString());
|
||||
if (isStdRefType(pt)) s << "const " << pt << "& " << p.value("name").toString();
|
||||
else s << pt << " " << p.value("name").toString();
|
||||
const QString qtPt = p.value("type").toString();
|
||||
const QString pt = paramTypeFor(qtPt, ApiStyle::Lp, rs);
|
||||
if (byRefFor(qtPt, pt, ApiStyle::Lp, rs)) s << "const " << pt << "& " << p.value("name").toString();
|
||||
else s << pt << " " << p.value("name").toString();
|
||||
if (i + 1 < params.size()) s << ", ";
|
||||
}
|
||||
};
|
||||
@@ -1087,12 +1403,12 @@ QString makeSourceLp(const QString& moduleName, const QString& className, const
|
||||
s << " nlohmann::json _args = nlohmann::json::array();\n";
|
||||
for (const QJsonValue& pv : params) {
|
||||
const QJsonObject p = pv.toObject();
|
||||
s << " _args.push_back(" << lpPushExpr(p.value("type").toString(), p.value("name").toString()) << ");\n";
|
||||
s << " _args.push_back(" << toWireFor(p.value("type").toString(), ApiStyle::Lp, rs, p.value("name").toString()) << ");\n";
|
||||
}
|
||||
};
|
||||
|
||||
// Sync
|
||||
s << ret << " " << className << "::" << name << "(";
|
||||
s << retQual << " " << className << "::" << name << "(";
|
||||
emitParams();
|
||||
if (!params.isEmpty()) s << ", ";
|
||||
s << "logos::CallError* err) {\n";
|
||||
@@ -1101,7 +1417,7 @@ QString makeSourceLp(const QString& moduleName, const QString& className, const
|
||||
s << " " << clientExpr << ".invoke(\"" << name << "\", _args, err);\n";
|
||||
} else {
|
||||
s << " nlohmann::json _r = " << clientExpr << ".invoke(\"" << name << "\", _args, err);\n";
|
||||
s << " return " << lpFromJsonExpr(qtRet, "_r") << ";\n";
|
||||
s << " return " << fromWireFor(qtRet, ApiStyle::Lp, rs, "_r", className + "::") << ";\n";
|
||||
}
|
||||
s << "}\n\n";
|
||||
|
||||
@@ -1119,7 +1435,7 @@ QString makeSourceLp(const QString& moduleName, const QString& className, const
|
||||
if (ret == "void") {
|
||||
s << " (void)_r; callback();\n";
|
||||
} else {
|
||||
s << " callback(" << lpFromJsonExpr(qtRet, "_r") << ");\n";
|
||||
s << " callback(" << fromWireFor(qtRet, ApiStyle::Lp, rs, "_r", className + "::") << ");\n";
|
||||
}
|
||||
s << " });\n";
|
||||
s << "}\n\n";
|
||||
|
||||
@@ -72,15 +72,25 @@ QString toQVariantConversion(const QString& type, const QString& argExpr);
|
||||
// runtime-bound interface wrapper (Bound) — see BindMode above. In Bound
|
||||
// mode `moduleName` is used only for the class/file naming the caller
|
||||
// already decided; the emitted code never bakes it into a call.
|
||||
QString makeHeader(const QString& moduleName, const QString& className, const QJsonArray& methods, ApiStyle apiStyle = ApiStyle::Qt, const QJsonArray& events = {}, BindMode bindMode = BindMode::Static);
|
||||
QString makeSource(const QString& moduleName, const QString& className, const QString& headerBaseName, const QJsonArray& methods, ApiStyle apiStyle = ApiStyle::Qt, const QJsonArray& events = {}, BindMode bindMode = BindMode::Static);
|
||||
//
|
||||
// `records` carries the contract's `type Foo { ... }` declarations, as
|
||||
// [ { "name": "Foo", "fields": [ { "name": "...", "type": "<QtTypeName>" } ] } ]
|
||||
// Each becomes a struct NESTED in the wrapper class (`<Class>::Foo`, so two
|
||||
// deps may both declare a `Status`), and every method / event that mentions
|
||||
// one is typed with it instead of falling back to QVariant / LogosMap. Field
|
||||
// types use the same Qt type-name spelling as methods, so a field can name
|
||||
// another record, `QList<Record>`, or `QMap<QString, Record>`. Empty (the
|
||||
// default, and what the metaobject-introspection path passes) leaves the
|
||||
// generated output exactly as it was.
|
||||
QString makeHeader(const QString& moduleName, const QString& className, const QJsonArray& methods, ApiStyle apiStyle = ApiStyle::Qt, const QJsonArray& events = {}, BindMode bindMode = BindMode::Static, const QJsonArray& records = {});
|
||||
QString makeSource(const QString& moduleName, const QString& className, const QString& headerBaseName, const QJsonArray& methods, ApiStyle apiStyle = ApiStyle::Qt, const QJsonArray& events = {}, BindMode bindMode = BindMode::Static, const QJsonArray& records = {});
|
||||
|
||||
// Qt-free (ApiStyle::Lp) wrapper emission. Same std-typed surface as the Std
|
||||
// flavor, but the generated body calls the logos-protocol C ABI through
|
||||
// logos::LpClient instead of LogosAPIClient — no Qt in the wrapper's TU.
|
||||
// makeHeader/makeSource dispatch here when apiStyle == ApiStyle::Lp.
|
||||
QString makeHeaderLp(const QString& moduleName, const QString& className, const QJsonArray& methods, const QJsonArray& events = {}, BindMode bindMode = BindMode::Static);
|
||||
QString makeSourceLp(const QString& moduleName, const QString& className, const QString& headerBaseName, const QJsonArray& methods, const QJsonArray& events = {}, BindMode bindMode = BindMode::Static);
|
||||
QString makeHeaderLp(const QString& moduleName, const QString& className, const QJsonArray& methods, const QJsonArray& events = {}, BindMode bindMode = BindMode::Static, const QJsonArray& records = {});
|
||||
QString makeSourceLp(const QString& moduleName, const QString& className, const QString& headerBaseName, const QJsonArray& methods, const QJsonArray& events = {}, BindMode bindMode = BindMode::Static, const QJsonArray& records = {});
|
||||
QVector<ParsedMethod> parseProviderHeader(const QString& headerPath, QTextStream& err);
|
||||
|
||||
#endif // GENERATOR_LIB_H
|
||||
|
||||
@@ -37,8 +37,12 @@ static QString lidlTypeExprToQtTypeName(const TypeExpr& te)
|
||||
if (te.kind == TypeExpr::Primitive) {
|
||||
if (te.name == "tstr") return "QString";
|
||||
if (te.name == "bstr") return "QByteArray";
|
||||
if (te.name == "int") return "int";
|
||||
if (te.name == "uint") return "int"; // wire-as-int for now
|
||||
// 64-bit, and unsigned stays unsigned. LIDL int/uint are int64_t /
|
||||
// uint64_t in every other binding; spelling them `int` here handed a
|
||||
// Qt-style consumer a signed 32-bit value (silent truncation above
|
||||
// 2^31) and a std/lp consumer a SIGNED int64_t for a `uint`.
|
||||
if (te.name == "int") return "qlonglong";
|
||||
if (te.name == "uint") return "qulonglong";
|
||||
if (te.name == "float64") return "double";
|
||||
if (te.name == "bool") return "bool";
|
||||
if (te.name == "result") return "LogosResult";
|
||||
@@ -49,20 +53,33 @@ static QString lidlTypeExprToQtTypeName(const TypeExpr& te)
|
||||
const TypeExpr& elem = te.elements[0];
|
||||
if (elem.kind == TypeExpr::Primitive && elem.name == "tstr")
|
||||
return "QStringList";
|
||||
// A list of records keeps its element type — the wrapper emits a
|
||||
// typed container, not a bag of QVariants.
|
||||
if (elem.kind == TypeExpr::Named)
|
||||
return "QList<" + qs(elem.name) + ">";
|
||||
return "QVariantList";
|
||||
}
|
||||
if (te.kind == TypeExpr::Map) return "QVariantMap";
|
||||
if (te.kind == TypeExpr::Map) {
|
||||
if (te.elements.size() == 2 && te.elements[1].kind == TypeExpr::Named)
|
||||
return "QMap<QString, " + qs(te.elements[1].name) + ">";
|
||||
return "QVariantMap";
|
||||
}
|
||||
if (te.kind == TypeExpr::Optional) return "QVariant";
|
||||
if (te.kind == TypeExpr::Named) return "QVariant";
|
||||
// A record declared by the contract: generator_lib emits the struct and
|
||||
// types every mention of it with the struct.
|
||||
if (te.kind == TypeExpr::Named) return qs(te.name);
|
||||
return "QVariant";
|
||||
}
|
||||
|
||||
static QJsonArray moduleRecordsToJson(const ModuleDecl& mod);
|
||||
|
||||
// Load events from a `.lidl` sidecar shipped alongside a module's
|
||||
// pre-built headers. Returns a JSON array of
|
||||
// { name, params: [ { name, type } ] }
|
||||
// using Qt-typed type names — same shape generator_lib's makeHeader /
|
||||
// makeSource already consume for methods.
|
||||
static QJsonArray loadEventsFromLidl(const QString& lidlPath, QTextStream& err)
|
||||
static QJsonArray loadEventsFromLidl(const QString& lidlPath, QTextStream& err,
|
||||
QJsonArray* outRecords = nullptr)
|
||||
{
|
||||
QJsonArray result;
|
||||
QFile f(lidlPath);
|
||||
@@ -80,6 +97,7 @@ static QJsonArray loadEventsFromLidl(const QString& lidlPath, QTextStream& err)
|
||||
return result;
|
||||
}
|
||||
|
||||
if (outRecords) *outRecords = moduleRecordsToJson(pr.module);
|
||||
for (const EventDecl& ed : pr.module.events) {
|
||||
QJsonObject obj;
|
||||
obj["name"] = qs(ed.name);
|
||||
@@ -171,6 +189,28 @@ static QJsonArray moduleMethodsToJson(const ModuleDecl& mod)
|
||||
return arr;
|
||||
}
|
||||
|
||||
// Build the records QJsonArray ({ name, fields:[{name,type}] }) from a parsed
|
||||
// ModuleDecl — the contract's `type Foo { ... }` declarations, which
|
||||
// generator_lib turns into structs nested in the wrapper class.
|
||||
static QJsonArray moduleRecordsToJson(const ModuleDecl& mod)
|
||||
{
|
||||
QJsonArray arr;
|
||||
for (const TypeDecl& td : mod.types) {
|
||||
QJsonObject o;
|
||||
o["name"] = qs(td.name);
|
||||
QJsonArray fields;
|
||||
for (const FieldDecl& fd : td.fields) {
|
||||
QJsonObject f;
|
||||
f["name"] = qs(fd.name);
|
||||
f["type"] = lidlTypeExprToQtTypeName(fd.type);
|
||||
fields.append(f);
|
||||
}
|
||||
o["fields"] = fields;
|
||||
arr.append(o);
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
// Build the events QJsonArray ({ name, params:[{name,type}] }) — same shape
|
||||
// loadEventsFromLidl produces — from a parsed ModuleDecl.
|
||||
static QJsonArray moduleEventsToJson(const ModuleDecl& mod)
|
||||
@@ -276,11 +316,12 @@ static bool generateInterfaceWrappers(const QVector<InterfaceSpec>& ifaces,
|
||||
const QString className = toPascalCase(spec.name);
|
||||
const QJsonArray methods = moduleMethodsToJson(mod);
|
||||
const QJsonArray events = moduleEventsToJson(mod);
|
||||
const QJsonArray records = moduleRecordsToJson(mod);
|
||||
const QString headerRel = spec.name + "_api.h";
|
||||
const QString sourceRel = spec.name + "_api.cpp";
|
||||
|
||||
const QString header = makeHeader(spec.name, className, methods, apiStyle, events, bindMode);
|
||||
const QString source = makeSource(spec.name, className, headerRel, methods, apiStyle, events, bindMode);
|
||||
const QString header = makeHeader(spec.name, className, methods, apiStyle, events, bindMode, records);
|
||||
const QString source = makeSource(spec.name, className, headerRel, methods, apiStyle, events, bindMode, records);
|
||||
|
||||
{
|
||||
QFile f(QDir(genDirPath).filePath(headerRel));
|
||||
@@ -773,7 +814,7 @@ static int generateProviderDispatch(const QString& headerPath, const QString& ou
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int generateFromPlugin(const QString& pluginInputPath, const QString& outputDir, bool moduleOnly, ApiStyle apiStyle, const QJsonArray& events, QTextStream& out, QTextStream& err)
|
||||
static int generateFromPlugin(const QString& pluginInputPath, const QString& outputDir, bool moduleOnly, ApiStyle apiStyle, const QJsonArray& events, QTextStream& out, QTextStream& err, const QJsonArray& records = {})
|
||||
{
|
||||
QFileInfo fi(pluginInputPath);
|
||||
if (!fi.exists()) {
|
||||
@@ -841,8 +882,8 @@ static int generateFromPlugin(const QString& pluginInputPath, const QString& out
|
||||
// doesn't need to know which style was picked. `events` (loaded
|
||||
// from a sibling `.lidl` sidecar via --events-from) adds typed
|
||||
// `on<EventName>(callback)` accessors next to the existing methods.
|
||||
QString header = makeHeader(moduleName, className, methods, apiStyle, events);
|
||||
QString source = makeSource(moduleName, className, headerRel, methods, apiStyle, events);
|
||||
QString header = makeHeader(moduleName, className, methods, apiStyle, events, BindMode::Static, records);
|
||||
QString source = makeSource(moduleName, className, headerRel, methods, apiStyle, events, BindMode::Static, records);
|
||||
|
||||
{
|
||||
QFile f(headerAbs);
|
||||
@@ -1195,6 +1236,7 @@ int legacy_main(int argc, char* argv[])
|
||||
// `on<EventName>(callback)` accessors next to the existing
|
||||
// generic `onEvent(name, callback)` channel.
|
||||
QJsonArray eventsFromSidecar;
|
||||
QJsonArray recordsFromSidecar;
|
||||
{
|
||||
const int evIdx = args.indexOf("--events-from");
|
||||
QString evPath;
|
||||
@@ -1209,10 +1251,10 @@ int legacy_main(int argc, char* argv[])
|
||||
}
|
||||
}
|
||||
if (!evPath.isEmpty() && QFileInfo(evPath).exists()) {
|
||||
eventsFromSidecar = loadEventsFromLidl(evPath, err);
|
||||
eventsFromSidecar = loadEventsFromLidl(evPath, err, &recordsFromSidecar);
|
||||
}
|
||||
}
|
||||
|
||||
QString argPath = args.at(1);
|
||||
return generateFromPlugin(argPath, outputDir, moduleOnly, apiStyle, eventsFromSidecar, out, err);
|
||||
return generateFromPlugin(argPath, outputDir, moduleOnly, apiStyle, eventsFromSidecar, out, err, recordsFromSidecar);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ add_executable(generator_tests
|
||||
test_make_header.cpp
|
||||
test_make_source.cpp
|
||||
test_parse_provider_header.cpp
|
||||
test_records.cpp
|
||||
)
|
||||
|
||||
target_include_directories(generator_tests PRIVATE
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
// Records on the consumer side of the LEGACY generator — the wrapper every
|
||||
// C++ module actually gets for its dependencies (`--dep <name>=<lidl>`).
|
||||
//
|
||||
// A contract's `type Status { ... }` used to reach every C++ consumer as an
|
||||
// untyped bag: QVariant on the Qt surface, LogosMap on the std/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 <gtest/gtest.h>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#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<Status>"),
|
||||
field("tags", "QMap<QString, Status>")};
|
||||
return QJsonArray{status, batch};
|
||||
}
|
||||
|
||||
QJsonArray statusMethods()
|
||||
{
|
||||
return QJsonArray{
|
||||
method("getStatus", "Status"),
|
||||
method("describeStatus", "QString", QJsonArray{param("s", "Status")}),
|
||||
method("listStatuses", "QList<Status>"),
|
||||
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<Status> items{};"));
|
||||
EXPECT_TRUE(h.contains(" QMap<QString, Status> 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<Status> listStatuses("));
|
||||
|
||||
// The old fallback is gone.
|
||||
EXPECT_FALSE(h.contains("QVariant getStatus("));
|
||||
EXPECT_FALSE(h.contains("describeStatus(QVariant"));
|
||||
}
|
||||
|
||||
// The std / lp surfaces spell the same records in std types — a universal
|
||||
// (Qt-free) module never sees a Qt name.
|
||||
TEST(Records, StdAndLpWrappersUseStdFieldTypes)
|
||||
{
|
||||
for (ApiStyle style : {ApiStyle::Std, 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<uint8_t> blob{};"));
|
||||
EXPECT_TRUE(h.contains(" std::vector<Status> items{};"));
|
||||
EXPECT_TRUE(h.contains(" std::map<std::string, Status> tags{};"));
|
||||
EXPECT_TRUE(h.contains("std::vector<Status> listStatuses("));
|
||||
// No LogosMap stand-in for a record.
|
||||
EXPECT_FALSE(h.contains("LogosMap getStatus("));
|
||||
}
|
||||
|
||||
// std::map needs its header on the std/lp surfaces.
|
||||
const QString lp = makeHeader("info_module", "InfoModule", statusMethods(),
|
||||
ApiStyle::Lp, {}, BindMode::Static, statusRecords());
|
||||
EXPECT_TRUE(lp.contains("#include <map>"));
|
||||
}
|
||||
|
||||
// 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: a std/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::Status> 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::Std, 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/Std); 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::Std, 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<void(const Status& s, qlonglong at)> 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))"));
|
||||
}
|
||||
Reference in New Issue
Block a user