diff --git a/README.md b/README.md index 1ee0177..1663686 100644 --- a/README.md +++ b/README.md @@ -242,6 +242,28 @@ into the API). The same applies to `interface: "provider"` modules whose methods are marked with `LOGOS_METHOD`. See `cpp-generator/docs/spec.md` → *Method documentation* for details. +**Documenting events:** events are the other half of a module's API — declared +in a `logos_events:` section and surfaced the same way. A doc comment above an +event declaration becomes that event's `description`. Events are reported +*inside* the generated `getMethods()` output (each entry tagged `type: "event"`, +methods tagged `type: "method"`); the framework exposes filtered views — +`getPluginMethods()`, `getPluginEvents()`, `getPluginInterface()` — so the event +surfaces in `lm events`, `logoscore module-info`'s Events section, and Basecamp's +Interface screen: + +```cpp +logos_events: + /// Emitted once the user has authenticated. + /// Carries the freshly issued session token. + void userLoggedIn(const std::string& userId, const std::string& token); +``` + +Event entries carry `type: "event"`, `name`, `signature`, `parameters[]`, and +`description` (no `returnType` — events are fire-and-forget). Folding events into +`getMethods()` rather than adding a `getEvents()` vtable method keeps the +provider ABI stable across SDK versions. See `cpp-generator/docs/spec.md` → +*Event documentation* for details. + Available getters: | Getter | Description | diff --git a/cpp-generator/docs/project.md b/cpp-generator/docs/project.md index f27be05..280ae9d 100644 --- a/cpp-generator/docs/project.md +++ b/cpp-generator/docs/project.md @@ -36,7 +36,7 @@ Shared data model used by all pipelines: - **`TypeExpr`** — type expression with `Kind` (Primitive, Array, Map, Optional, Named), `name`, and `elements` - **`ParamDecl`** — parameter name + type - **`MethodDecl`** — method name, params, return type, `description` (doc comment above the declaration, emitted into `getMethods()`), `jsonReturn` flag (true when impl returns `LogosMap`/`LogosList`) -- **`EventDecl`** — event name + params +- **`EventDecl`** — event name, params, `description` (doc comment above the `logos_events:` declaration, emitted as a `type: "event"` entry inside `getMethods()`) - **`FieldDecl`** — struct field name, type, optional flag - **`TypeDecl`** — named struct type with fields - **`ModuleDecl`** — complete module: name, version, description, category, depends, types, methods, events @@ -127,7 +127,7 @@ Flag plumbing: - Emits `nlohmannToQVariant()` helper when any method has `jsonReturn = true` - Always emits an `onInit(LogosAPI*) override` that, via SFINAE'd helpers in `logos_module_context.h`, (a) copies the three runtime-injected properties (`modulePath`, `instanceId`, `instancePersistencePath`) into the impl, (b) constructs a per-module `LogosModules` aggregate and threads its pointer through the same base, and (c) installs the typed-event callback (`maybeSetEmitEvent`) consumed by `_events.cpp` method bodies. Impls that don't inherit `LogosModuleContext` compile unchanged — the helper overloads collapse to no-ops. The full `LogosAPI` is never exposed past the provider boundary. - Always emits `#include "logos_sdk.h"` and a `std::unique_ptr m_logosModules` member; ownership lives on the provider, the context base sees only a non-owning `void*` reinterpreted in `LogosModuleContext::modules()` (which depends on the impl's TU having included `logos_sdk.h`). -- `lidlMakeProviderDispatch(ModuleDecl)` — generates callMethod/getMethods dispatch +- `lidlMakeProviderDispatch(ModuleDecl)` — generates callMethod/getMethods dispatch. `getMethods()` emits the full interface: each method tagged `type: "method"`, then each `module.events` entry tagged `type: "event"` (name, signature, parameters, escaped `description`; no returnType/isInvokable). There is no separate `getEvents()` — folding events into `getMethods()` keeps the provider vtable ABI-stable. - `lidlMakeEventsSource(ModuleDecl, implClass, implHeader)` — generates `_events.cpp`: Qt-MOC-style method bodies for prototypes declared in the impl's `logos_events:` block. Each body marshals typed args into a `QVariantList` and calls `this->emitEventImpl_("", &args)` on the LogosModuleContext base. - `lidlGenerateProviderGlue(lidlPath, ...)` — full pipeline from .lidl file. Also emits `_events.cpp` and a `.lidl` sidecar (via `lidlSerialize`) when the module has any events; both ride the dep's `headers-*` outputs to power consumer-side typed `on()` accessors. @@ -135,7 +135,7 @@ Flag plumbing: - `parseImplHeader(headerPath, className, metadataPath, err)` — parses C++ header + metadata.json into ModuleDecl - State machine: `LookingForClass` → `InClass` → `InPublic`/`InPrivate`/`InLogosEvents` -- The literal `logos_events:` token (defined in `logos_module_context.h` as `#define logos_events public`) opens an events section; bare prototypes inside become `EventDecl{name, params}` entries appended to `ModuleDecl.events` +- The literal `logos_events:` token (defined in `logos_module_context.h` as `#define logos_events public`) opens an events section; bare prototypes inside become `EventDecl{name, params, description}` entries appended to `ModuleDecl.events` (the `description` is the doc comment immediately above the declaration, captured via `joinDocLines` exactly as for methods) - Skips: constructors, destructors, typedefs, using, friend, enum, struct, `std::function` declarations - Recognizes `LogosMap` and `LogosList` return types (nlohmann::json aliases) and sets `MethodDecl.jsonReturn = true` - Template-aware parameter splitting (handles `std::vector` correctly) diff --git a/cpp-generator/docs/spec.md b/cpp-generator/docs/spec.md index af823ef..b0a5e0f 100644 --- a/cpp-generator/docs/spec.md +++ b/cpp-generator/docs/spec.md @@ -180,6 +180,55 @@ introspected purely via Qt's `QMetaObject` (legacy `Q_INVOKABLE` modules with no generated dispatch) carry no comments at runtime and therefore have no `description`. +### Event documentation + +Events are the subscribe-half of a module's API (methods are the call-half), and +document the same way. A doc comment directly above an event declaration in the +`logos_events:` section (see [Event Emission](#event-emission-via-logos_events) +below) becomes that event's `description`, stored on `EventDecl.description` in +the shared AST and emitted into the `description` field of the event's entry in +**`getMethods()`**. + +`getMethods()` returns the module's *whole* interface — methods **and** events — +with each entry tagged by a `"type"` field (`"method"` or `"event"`). Events ride +inside `getMethods()` deliberately: there is **no** separate `getEvents()` vtable +method, so `LogosProviderObject`'s vtable layout never shifts and old/new hosts +and modules stay binary-compatible (see *Why events live in `getMethods()`* +below). The framework then offers three filtered views over that one call — +`getPluginMethods()` (entries that aren't events), `getPluginEvents()` +(`type == "event"`), and `getPluginInterface()` (everything) — so the +description flows, with no extra provider call, to `lm events`, `logoscore +module-info`'s Events section, and Basecamp's Interface screen. + +The capture rules are identical to methods: only `///` line comments and +`/** … */` / `/*! … */` block comments are captured (plain `//` and `/* … */` +are ignored); multi-line comments preserve their line breaks (markers stripped, +joined with `\n`, leading/trailing blanks dropped); only comments immediately +adjacent to the declaration attach. + +```cpp +logos_events: + /// Emitted once the user has authenticated. + /// Carries the freshly issued session token. + void userLoggedIn(const std::string& userId, const std::string& token); +``` + +→ the `userLoggedIn` entry in `getMethods()` gains +`"type": "event"` and +`"description": "Emitted once the user has authenticated.\nCarries the freshly issued session token."` + +An event entry carries `type: "event"`, `name`, `signature`, `parameters[]` +(each with `type` and `name`), and — when documented — `description`. Unlike a +method entry it has no `returnType` or `isInvokable`: events are void, +fire-and-forget. Events are a universal (`--from-header`) concept; the legacy +`--provider-header` path declares none, so its `getMethods()` contains only +methods. (An entry with no `"type"` is treated as a method, so a module built +against a pre-events SDK simply reports zero events.) + +An event's `description` may also be supplied out-of-band via an optional +`description` field on the corresponding `metadata.json` `events[]` entry (the +doc comment takes the same role for both sources). + ### Event Emission via `logos_events:` Universal modules declare events in a Qt-`signals:`-style section parsed by the codegen. The same method name appears on both sides — declared in `logos_events:`, called directly to emit: @@ -255,7 +304,15 @@ Contains two classes: Implements two methods on the ProviderObject: 1. `**callMethod(methodName, args)`** — string-based dispatch table. For each method, extracts args from `QVariantList`, calls the typed wrapper, returns result as `QVariant`. Void methods return `QVariant(true)`. -2. `**getMethods()**` — returns `QJsonArray` of method metadata. Each entry has `name`, `signature`, `returnType`, `isInvokable`, and `parameters[]` (with `type` and `name`). When the method's declaration in the impl header is preceded by a doc comment, the entry also carries a `description` (see [Method documentation](#method-documentation) below). This array is what the framework's `getPluginMethods()` returns, so the `description` surfaces in `lm methods`, `logoscore module-info`, and Basecamp's Methods list. +2. `**getMethods()**` — returns a `QJsonArray` describing the module's **whole interface — both methods and events**. Each entry carries a `"type"` of `"method"` or `"event"`: + - **method** entries have `type: "method"`, `name`, `signature`, `returnType`, `isInvokable`, `parameters[]` (with `type` and `name`), and — when the declaration has a doc comment — `description` (see [Method documentation](#method-documentation)). + - **event** entries (one per `logos_events:` declaration) have `type: "event"`, `name`, `signature`, `parameters[]`, and an optional `description` (see [Event documentation](#event-documentation)). They omit `returnType`/`isInvokable` — events are void. + + The framework slices this single array into `getPluginMethods()` (non-event entries), `getPluginEvents()` (`type == "event"`), and `getPluginInterface()` (everything), which is what surfaces in `lm methods`/`lm events`, `logoscore module-info`, and Basecamp's Interface screen. + +##### Why events live in `getMethods()` + +Folding events into `getMethods()` — rather than adding a sibling `getEvents()` virtual — is a deliberate **ABI** choice. `LogosProviderObject` is the in-process vtable contract between a host/runtime and a loaded module; inserting a new virtual would shift every later vtable slot and break any mix of old/new host and module binaries. Reusing the existing `getMethods()` slot keeps the vtable byte-for-byte stable: a new host reading an old module just sees no `type: "event"` entries (so zero events), and an old host reading a new module ignores the `"type"` field (events show up in its method list — cosmetic, never a crash). Legacy `--provider-header` and Qt modules declare no events, so their `getMethods()` is methods-only. #### Client Stubs (`_api.h` + `_api.cpp`) diff --git a/cpp-generator/experimental/impl_header_parser.cpp b/cpp-generator/experimental/impl_header_parser.cpp index 22d72de..4e5d618 100644 --- a/cpp-generator/experimental/impl_header_parser.cpp +++ b/cpp-generator/experimental/impl_header_parser.cpp @@ -233,6 +233,7 @@ ImplParseResult parseImplHeader(const QString& headerPath, QJsonObject evObj = ev.toObject(); EventDecl ed; ed.name = evObj.value("name").toString(); + ed.description = evObj.value("description").toString(); QJsonArray params = evObj.value("params").toArray(); for (const QJsonValue& pv : params) { QJsonObject po = pv.toObject(); @@ -396,6 +397,7 @@ ImplParseResult parseImplHeader(const QString& headerPath, EventDecl ed; ed.name = md.name; ed.params = md.params; + ed.description = joinDocLines(pendingDoc); result.module.events.append(ed); } } diff --git a/cpp-generator/experimental/lidl_ast.h b/cpp-generator/experimental/lidl_ast.h index d4d05ef..b29b70a 100644 --- a/cpp-generator/experimental/lidl_ast.h +++ b/cpp-generator/experimental/lidl_ast.h @@ -59,9 +59,11 @@ struct MethodDecl { struct EventDecl { QString name; QVector params; + // Doc comment adjacent to the event declaration (becomes "description"). + QString description; bool operator==(const EventDecl& o) const { - return name == o.name && params == o.params; + return name == o.name && params == o.params && description == o.description; } }; diff --git a/cpp-generator/experimental/lidl_gen_provider.cpp b/cpp-generator/experimental/lidl_gen_provider.cpp index 8bc8608..e6f5bd3 100644 --- a/cpp-generator/experimental/lidl_gen_provider.cpp +++ b/cpp-generator/experimental/lidl_gen_provider.cpp @@ -486,6 +486,7 @@ QString lidlMakeProviderDispatch(const ModuleDecl& module) QString qtRet = lidlTypeToQt(md.returnType); s << " {\n"; s << " QJsonObject obj;\n"; + s << " obj[\"type\"] = QStringLiteral(\"method\");\n"; s << " obj[\"name\"] = QStringLiteral(\"" << md.name << "\");\n"; s << " obj[\"returnType\"] = QStringLiteral(\"" << qtRet << "\");\n"; s << " obj[\"isInvokable\"] = true;\n"; @@ -519,6 +520,47 @@ QString lidlMakeProviderDispatch(const ModuleDecl& module) s << " }\n"; } + // Events are appended to the SAME interface list, tagged type "event" (and + // with no returnType/isInvokable — they are void/fire-and-forget). Folding + // them into getMethods() instead of adding a getEvents() vtable slot keeps + // LogosProviderObject's vtable layout stable, so old/new hosts and modules + // stay binary-compatible. Callers split the list back out by "type" (see + // ModuleProxy::getPluginMethods/getPluginEvents/getPluginInterface). + for (const EventDecl& ed : module.events) { + s << " {\n"; + s << " QJsonObject obj;\n"; + s << " obj[\"type\"] = QStringLiteral(\"event\");\n"; + s << " obj[\"name\"] = QStringLiteral(\"" << ed.name << "\");\n"; + if (!ed.description.isEmpty()) { + QString escDesc = ed.description; + escDesc.replace('\\', "\\\\"); + escDesc.replace('"', "\\\""); + escDesc.replace('\n', "\\n"); + s << " obj[\"description\"] = QStringLiteral(\"" << escDesc << "\");\n"; + } + + QString sig = ed.name + "("; + for (int i = 0; i < ed.params.size(); ++i) { + sig += lidlTypeToQt(ed.params[i].type); + if (i + 1 < ed.params.size()) sig += ","; + } + sig += ")"; + s << " obj[\"signature\"] = QStringLiteral(\"" << sig << "\");\n"; + + if (!ed.params.isEmpty()) { + s << " QJsonArray params;\n"; + for (int i = 0; i < ed.params.size(); ++i) { + s << " params.append(QJsonObject{{\"type\", QStringLiteral(\"" + << lidlTypeToQt(ed.params[i].type) << "\")}, {\"name\", QStringLiteral(\"" + << ed.params[i].name << "\")}});\n"; + } + s << " obj[\"parameters\"] = params;\n"; + } + + s << " methods.append(obj);\n"; + s << " }\n"; + } + s << " return methods;\n"; s << "}\n"; diff --git a/cpp/logos_provider_object.h b/cpp/logos_provider_object.h index 44f8ce8..545e45a 100644 --- a/cpp/logos_provider_object.h +++ b/cpp/logos_provider_object.h @@ -39,6 +39,14 @@ public: // --- Qt interface (pure virtual — existing providers override these) --- virtual QVariant callMethod(const QString& methodName, const QVariantList& args) = 0; virtual bool informModuleToken(const QString& moduleName, const QString& token) = 0; + // Returns the module's full interface as a QJsonArray: both methods and + // events, each entry tagged with a "type" of "method" or "event" (events + // omit returnType/isInvokable — they are void/fire-and-forget). Events ride + // inside getMethods() ON PURPOSE: this avoids adding a separate getEvents() + // vtable slot, so the vtable layout never shifts and old/new hosts and + // modules stay binary-compatible. An entry with no "type" is a method (so + // pre-events modules degrade cleanly). Callers split the list by "type" + // (see ModuleProxy::getPluginMethods/getPluginEvents/getPluginInterface). virtual QJsonArray getMethods() = 0; virtual void setEventListener(EventCallback callback) = 0; virtual void init(void* apiInstance) = 0; diff --git a/cpp/module_proxy.cpp b/cpp/module_proxy.cpp index 2bccc1f..c22647a 100644 --- a/cpp/module_proxy.cpp +++ b/cpp/module_proxy.cpp @@ -1,6 +1,8 @@ #include "module_proxy.h" #include "logos_provider_object.h" #include +#include +#include ModuleProxy::ModuleProxy(LogosProviderObject* provider, QObject* parent) : QObject(parent) @@ -66,6 +68,14 @@ QVariant ModuleProxy::callRemoteMethod(const QString& authToken, const QString& return QVariant(getPluginMethods()); } + if (methodName == "getPluginEvents" && args.isEmpty()) { + return QVariant(getPluginEvents()); + } + + if (methodName == "getPluginInterface" && args.isEmpty()) { + return QVariant(getPluginInterface()); + } + qDebug() << "ModuleProxy: callRemoteMethod" << methodName << "args:" << args; return m_provider->callMethod(methodName, args); } @@ -82,7 +92,24 @@ bool ModuleProxy::informModuleToken(const QString& authToken, const QString& mod return m_provider->informModuleToken(moduleName, token); } -QJsonArray ModuleProxy::getPluginMethods() +namespace { +// getMethods() returns the module's full interface — both methods and events, +// each tagged with a "type" ("method"/"event"). Split it back out. An entry +// with no "type" counts as a method, so modules built against the pre-events +// SDK (whose getMethods() contains no events) report zero events, not a crash. +QJsonArray filterInterface(const QJsonArray& interface, bool keepEvents) +{ + QJsonArray out; + for (const QJsonValue& v : interface) { + const bool isEvent = + v.toObject().value(QStringLiteral("type")).toString() == QStringLiteral("event"); + if (isEvent == keepEvents) out.append(v); + } + return out; +} +} // namespace + +QJsonArray ModuleProxy::getPluginInterface() { if (!m_provider) return QJsonArray(); @@ -90,4 +117,14 @@ QJsonArray ModuleProxy::getPluginMethods() return m_provider->getMethods(); } +QJsonArray ModuleProxy::getPluginMethods() +{ + return filterInterface(getPluginInterface(), /*keepEvents=*/false); +} + +QJsonArray ModuleProxy::getPluginEvents() +{ + return filterInterface(getPluginInterface(), /*keepEvents=*/true); +} + #include "moc_module_proxy.cpp" diff --git a/cpp/module_proxy.h b/cpp/module_proxy.h index dcae013..7ff2d84 100644 --- a/cpp/module_proxy.h +++ b/cpp/module_proxy.h @@ -30,7 +30,14 @@ public: Q_INVOKABLE QVariant callRemoteMethod(const QString& authToken, const QString& methodName, const QVariantList& args = QVariantList()); Q_INVOKABLE bool informModuleToken(const QString& authToken, const QString& moduleName, const QString& token); bool saveToken(const QString& from_module_name, const QString& token); + // getPluginInterface() returns the module's whole interface (methods AND + // events, each tagged with a "type"); getPluginMethods()/getPluginEvents() + // are the type-filtered views. All three derive from the provider's single + // getMethods() call — there is no separate getEvents() vtable method, which + // is what keeps the provider ABI stable across SDK versions. Q_INVOKABLE QJsonArray getPluginMethods(); + Q_INVOKABLE QJsonArray getPluginEvents(); + Q_INVOKABLE QJsonArray getPluginInterface(); signals: void eventResponse(const QString& eventName, const QVariantList& data); diff --git a/cpp/qt_provider_object.cpp b/cpp/qt_provider_object.cpp index bb13d5c..0a91004 100644 --- a/cpp/qt_provider_object.cpp +++ b/cpp/qt_provider_object.cpp @@ -261,6 +261,18 @@ QVariant QtProviderObject::callMethod(const QString& methodName, const QVariantL return QVariant(getMethods()); } + // Special-case getPluginEvents / getPluginInterface. Legacy Qt modules have + // no logos_events: section, so getMethods() (built here from QMetaObject) + // only ever contains methods: events are always empty and the interface is + // just the methods list. + if (methodName == "getPluginEvents" && args.isEmpty()) { + return QVariant(QJsonArray()); + } + + if (methodName == "getPluginInterface" && args.isEmpty()) { + return QVariant(getMethods()); + } + // Auth-token validation (mirrors the old ModuleProxy logic) PluginInterface* pluginInterface = qobject_cast(m_module); if (!pluginInterface) { diff --git a/docs/docs.md b/docs/docs.md index 36b5c56..bcfd81a 100644 --- a/docs/docs.md +++ b/docs/docs.md @@ -202,6 +202,7 @@ Modules never instantiate `ModuleProxy` directly; it is created by the provider - Validate the authentication token on every remote call. In `callRemoteMethod()` the proxy checks that a non‑empty token is provided and verifies it against the `TokenManager`. Calls with invalid or missing tokens return an empty `QVariant`. - Dispatch method calls to the underlying module using Qt’s meta‑object system. The proxy locates the requested method by name and argument count, supports up to five arguments, and handles various return types including `void`, `bool`, `int`, `QString`, `QVariant`, `QJsonArray` and `QStringList` - Introspect the wrapped module’s API via `getPluginMethods()`, returning a `QJsonArray` describing each method (name, signature, return type, parameters, and — when the method has a doc comment in its header — a `description`) +- Introspect the wrapped module’s events via `getPluginEvents()`, returning a `QJsonArray` describing each `logos_events:` declaration (name, signature, parameters, and — when documented — a `description`; no return type, since events are void). `getPluginInterface()` returns both methods and events in one array (each entry tagged with a `"type"`). All three are filtered views over the provider's single `getMethods()` call — there is no separate `getEvents()` vtable method, which keeps the provider ABI stable across SDK versions - Provide an `eventResponse` signal that the provider emits when events are forwarded to subscribers - Store tokens issued by other modules via `saveToken(fromModuleName, token)` - Allow a module or consumer to inform another module of a token via `informModuleToken(authToken, moduleName, token)` @@ -212,6 +213,8 @@ Modules never instantiate `ModuleProxy` directly; it is created by the provider | `QVariant callRemoteMethod(const QString& authToken, const QString& methodName, const QVariantList& args = {})` | Validates `authToken`, locates `methodName` on the module and invokes it. Supports up to five arguments and multiple return types. This will forward the request to the wrapped object. | | `bool informModuleToken(const QString& authToken, const QString& moduleName, const QString& token)` | Stores `token` for `moduleName` in the global `TokenManager`. This is used by the core and capability module to let this module know that another module will communicate using a certain token,=. | | `QJsonArray getPluginMethods()` | Enumerates the wrapped module’s methods and returns a JSON array with signatures and parameters. Generated provider/universal modules also include a per-method `description` (from the method's header doc comment); legacy modules introspected via Qt meta‑object have none. | +| `QJsonArray getPluginEvents()` | Enumerates the wrapped module’s `logos_events:` declarations and returns a JSON array with names, signatures, and parameters (plus a per-event `description` from the declaration's doc comment). Universal modules report their declared events; legacy/provider modules return an empty array. | +| `QJsonArray getPluginInterface()` | Returns the module’s whole interface — methods and events together — each entry tagged with a `"type"` (`"method"`/`"event"`). `getPluginMethods`/`getPluginEvents` are the filtered views; all three derive from one `getMethods()` call (no separate `getEvents()` vtable method, so the provider ABI stays stable). | | `eventResponse(QString eventName, QVariantList data)` (signal) | Emitted when the proxy forwards an event to subscribers. | Example: Listing methods of a module (from a consumer) @@ -479,6 +482,8 @@ public: const QVariantList& args = {}); bool informModuleToken(const QString& authToken, const QString& moduleName, const QString& token); QJsonArray getPluginMethods(); + QJsonArray getPluginEvents(); + QJsonArray getPluginInterface(); signals: void eventResponse(const QString& eventName, const QVariantList& data); @@ -492,10 +497,12 @@ signals: | `callRemoteMethod(authToken, methodName, args) → QVariant` | Validates `authToken`, locates `methodName` on the module and invokes it | | `informModuleToken(authToken, moduleName, token) → bool` | Stores `token` for `moduleName` in the global `TokenManager` | | `getPluginMethods() → QJsonArray` | Enumerates the wrapped module's methods (name, signature, return type, parameters, and a per-method `description` for documented provider/universal methods) | +| `getPluginEvents() → QJsonArray` | Enumerates the wrapped module's `logos_events:` declarations (name, signature, parameters, and a per-event `description` for documented universal events); empty for legacy/provider modules | +| `getPluginInterface() → QJsonArray` | Methods and events together, each tagged with a `"type"`; the un-filtered source that `getPluginMethods`/`getPluginEvents` slice (all three come from one `getMethods()` call — no separate `getEvents()` vtable method) | **Responsibilities**: - Enforce token validation on every inbound call (returns invalid `QVariant` on failure). -- Dispatch to the wrapped QObject via Qt meta-object APIs and support introspection via `getPluginMethods()`. +- Dispatch to the wrapped QObject via Qt meta-object APIs and support introspection via `getPluginMethods()` / `getPluginEvents()`. - Provide the `eventResponse` signal used by providers/clients to forward events across process boundaries. ### 3.4 Generated Wrappers diff --git a/tests/experimental/fixtures/documented_events_impl.h b/tests/experimental/fixtures/documented_events_impl.h new file mode 100644 index 0000000..b61b767 --- /dev/null +++ b/tests/experimental/fixtures/documented_events_impl.h @@ -0,0 +1,23 @@ +#pragma once +#include + +// Parser tests only read this as text. Exercises doc-comment capture on +// events declared in a `logos_events:` section (mirrors how `///` comments +// above methods become a method's description). +class DocumentedEventsImpl { +public: + DocumentedEventsImpl() = default; + + void doWork(); + +logos_events: + /// Fired once the user has authenticated. + /// Carries the freshly issued session token. + void userLoggedIn(const std::string& userId, const std::string& token); + + // Plain (non-doc) comment — must NOT be captured as a description. + void heartbeat(); + + /// Single-line documented event. + void shutdown(); +}; diff --git a/tests/experimental/fixtures/documented_events_metadata.json b/tests/experimental/fixtures/documented_events_metadata.json new file mode 100644 index 0000000..43cb6bc --- /dev/null +++ b/tests/experimental/fixtures/documented_events_metadata.json @@ -0,0 +1,7 @@ +{ + "name": "documented_events_mod", + "version": "1.0.0", + "description": "Fixture for event doc-comment capture", + "category": "test", + "dependencies": [] +} diff --git a/tests/experimental/fixtures/universal_metadata.json b/tests/experimental/fixtures/universal_metadata.json index 8654c6f..59f216e 100644 --- a/tests/experimental/fixtures/universal_metadata.json +++ b/tests/experimental/fixtures/universal_metadata.json @@ -7,6 +7,7 @@ "events": [ { "name": "onReady", + "description": "Fired once the module is ready.", "params": [ { "name": "info", "type": "std::string" } ] diff --git a/tests/experimental/test_impl_header_parser.cpp b/tests/experimental/test_impl_header_parser.cpp index e8a6ed2..167b4c0 100644 --- a/tests/experimental/test_impl_header_parser.cpp +++ b/tests/experimental/test_impl_header_parser.cpp @@ -258,6 +258,8 @@ TEST_F(ImplHeaderParserTest, UniversalTypesAndMetadataEvents) ASSERT_EQ(r.module.events.size(), 1); EXPECT_EQ(r.module.events[0].name, "onReady"); + // Optional per-event description carried from metadata.json events[]. + EXPECT_EQ(r.module.events[0].description, "Fired once the module is ready."); ASSERT_EQ(r.module.events[0].params.size(), 1); EXPECT_EQ(r.module.events[0].params[0].name, "info"); EXPECT_EQ(r.module.events[0].params[0].type.name, "tstr"); @@ -312,3 +314,38 @@ TEST_F(ImplHeaderParserTest, UniversalTypesAndMetadataEvents) EXPECT_NE(m.name, "void") << "Keyword should not appear as method name"; } } + +// --------------------------------------------------------------------------- +// Event doc comments: `///` above a `logos_events:` declaration becomes the +// event's description (same capture rules as methods: doc-comments only, +// adjacent-only, multi-line joined with \n). +// --------------------------------------------------------------------------- + +TEST_F(ImplHeaderParserTest, EventDocCommentsFromHeader) +{ + auto r = parseImplHeader( + fixturesDir() + "/documented_events_impl.h", + "DocumentedEventsImpl", + fixturesDir() + "/documented_events_metadata.json", + err); + ASSERT_FALSE(r.hasError()) << r.error.toStdString(); + + ASSERT_EQ(r.module.events.size(), 3); + + // Multi-line `///` doc comment: the two lines are joined with a newline. + EXPECT_EQ(r.module.events[0].name, "userLoggedIn"); + EXPECT_EQ(r.module.events[0].description, + "Fired once the user has authenticated.\n" + "Carries the freshly issued session token."); + ASSERT_EQ(r.module.events[0].params.size(), 2); + EXPECT_EQ(r.module.events[0].params[0].name, "userId"); + EXPECT_EQ(r.module.events[0].params[1].name, "token"); + + // A plain `//` comment is not a doc comment → no description captured. + EXPECT_EQ(r.module.events[1].name, "heartbeat"); + EXPECT_TRUE(r.module.events[1].description.isEmpty()); + + // Single-line `///` doc comment. + EXPECT_EQ(r.module.events[2].name, "shutdown"); + EXPECT_EQ(r.module.events[2].description, "Single-line documented event."); +} diff --git a/tests/experimental/test_lidl_gen_provider.cpp b/tests/experimental/test_lidl_gen_provider.cpp index efc23f4..3bd1d26 100644 --- a/tests/experimental/test_lidl_gen_provider.cpp +++ b/tests/experimental/test_lidl_gen_provider.cpp @@ -375,4 +375,88 @@ TEST(LidlGenProvider, EmptyModuleGeneratesValidCode) QString d = lidlMakeProviderDispatch(m); EXPECT_TRUE(d.contains("::callMethod(")); EXPECT_TRUE(d.contains("::getMethods()")); + // There is no separate getEvents(): events (when present) ride inside + // getMethods(), so the provider vtable never gains a slot. + EXPECT_FALSE(d.contains("::getEvents()")); +} + +// --------------------------------------------------------------------------- +// Event introspection generation — events are folded INTO getMethods() (each +// tagged type "event"), not a separate getEvents() vtable method. This keeps +// LogosProviderObject's vtable layout stable across SDK versions. +// --------------------------------------------------------------------------- + +static ModuleDecl makeEventModule() +{ + ModuleDecl m; + m.name = "evt_module"; + m.version = "1.0.0"; + + // Documented event with two params (multi-line description). + { + EventDecl ed; + ed.name = "userLoggedIn"; + ed.description = "Auth done.\nToken issued."; + ParamDecl p1; p1.name = "userId"; p1.type = { TypeExpr::Primitive, "tstr", {} }; + ParamDecl p2; p2.name = "token"; p2.type = { TypeExpr::Primitive, "tstr", {} }; + ed.params.append(p1); + ed.params.append(p2); + m.events.append(ed); + } + // Undocumented, no-arg event. + { + EventDecl ed; + ed.name = "tick"; + m.events.append(ed); + } + return m; +} + +TEST(LidlGenProvider, GetMethodsContainsEventsTaggedEvent) +{ + auto m = makeEventModule(); + QString d = lidlMakeProviderDispatch(m); + // No separate getEvents() override is generated… + EXPECT_FALSE(d.contains("::getEvents()")); + // …instead events appear inside getMethods(), each tagged type "event". + EXPECT_TRUE(d.contains("::getMethods()")); + EXPECT_TRUE(d.contains("QStringLiteral(\"event\")")); + EXPECT_TRUE(d.contains("\"userLoggedIn\"")); + EXPECT_TRUE(d.contains("\"tick\"")); +} + +TEST(LidlGenProvider, GetMethodsEventHasSignatureAndParams) +{ + auto m = makeEventModule(); + QString d = lidlMakeProviderDispatch(m); + // Event signature is computed from its params (tstr → QString). + EXPECT_TRUE(d.contains("userLoggedIn(QString,QString)")); + EXPECT_TRUE(d.contains("\"userId\"")); + EXPECT_TRUE(d.contains("\"token\"")); +} + +TEST(LidlGenProvider, GetMethodsEventEmitsDescription) +{ + auto m = makeEventModule(); + QString d = lidlMakeProviderDispatch(m); + // The multi-line description is emitted with its newline escaped (\n). + EXPECT_TRUE(d.contains("Auth done.\\nToken issued.")); +} + +TEST(LidlGenProvider, EventEntriesHaveNoReturnType) +{ + // An events-only module: events are void, so no returnType/isInvokable key + // is emitted anywhere (those belong to methods only). + auto m = makeEventModule(); + QString d = lidlMakeProviderDispatch(m); + EXPECT_FALSE(d.contains("\"returnType\"")); + EXPECT_FALSE(d.contains("\"isInvokable\"")); +} + +TEST(LidlGenProvider, MethodEntriesTaggedMethod) +{ + // A module with methods: each getMethods() entry is tagged type "method". + auto m = makeTestModule(); + QString d = lidlMakeProviderDispatch(m); + EXPECT_TRUE(d.contains("QStringLiteral(\"method\")")); } diff --git a/tests/sdk/test_module_proxy.cpp b/tests/sdk/test_module_proxy.cpp index 0f7acea..17b8ce4 100644 --- a/tests/sdk/test_module_proxy.cpp +++ b/tests/sdk/test_module_proxy.cpp @@ -19,12 +19,23 @@ public: return returnValue; } + // getMethods() returns the whole interface: methods AND events, each tagged + // with a "type". The proxy slices it into getPluginMethods/Events/Interface. QJsonArray getMethods() override { QJsonArray arr; - QJsonObject m; - m["name"] = "testMethod"; - arr.append(m); + { + QJsonObject m; + m["type"] = "method"; + m["name"] = "testMethod"; + arr.append(m); + } + { + QJsonObject e; + e["type"] = "event"; + e["name"] = "testEvent"; + arr.append(e); + } return arr; } @@ -64,8 +75,26 @@ TEST_F(ModuleProxyTest, CallRemoteMethodDispatchesToProvider) TEST_F(ModuleProxyTest, GetPluginMethodsDispatchesToProvider) { ModuleProxy proxy(m_provider); + // getPluginMethods() returns the method-typed entries only — the event the + // provider also reports through getMethods() is filtered out. QJsonArray methods = proxy.getPluginMethods(); - EXPECT_EQ(methods.size(), 1); + ASSERT_EQ(methods.size(), 1); + EXPECT_EQ(methods[0].toObject()["name"].toString(), "testMethod"); +} + +TEST_F(ModuleProxyTest, GetPluginEventsReturnsOnlyEvents) +{ + ModuleProxy proxy(m_provider); + QJsonArray events = proxy.getPluginEvents(); + ASSERT_EQ(events.size(), 1); + EXPECT_EQ(events[0].toObject()["name"].toString(), "testEvent"); +} + +TEST_F(ModuleProxyTest, GetPluginInterfaceReturnsMethodsAndEvents) +{ + ModuleProxy proxy(m_provider); + // The whole interface — both the method and the event — in one array. + EXPECT_EQ(proxy.getPluginInterface().size(), 2); } TEST_F(ModuleProxyTest, GetPluginMethodsSpecialCaseInCallRemoteMethod) @@ -78,6 +107,20 @@ TEST_F(ModuleProxyTest, GetPluginMethodsSpecialCaseInCallRemoteMethod) EXPECT_TRUE(m_provider->lastMethodCalled.isEmpty()); } +TEST_F(ModuleProxyTest, GetPluginEventsAndInterfaceSpecialCaseInCallRemoteMethod) +{ + ModuleProxy proxy(m_provider); + + QVariant ev = proxy.callRemoteMethod("token", "getPluginEvents"); + EXPECT_EQ(ev.toJsonArray().size(), 1); + + QVariant iface = proxy.callRemoteMethod("token", "getPluginInterface"); + EXPECT_EQ(iface.toJsonArray().size(), 2); + + // Both are intercepted by the proxy, never dispatched to the provider. + EXPECT_TRUE(m_provider->lastMethodCalled.isEmpty()); +} + TEST_F(ModuleProxyTest, NullProviderHandling) { ModuleProxy proxy(nullptr);