From f803bb4cb04b4b5b29d987f3edeef898bcd82416 Mon Sep 17 00:00:00 2001 From: Dario Gabriel Lipicar Date: Thu, 4 Jun 2026 13:24:39 -0300 Subject: [PATCH] Add per-event documentation + getPluginEvents introspection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirror the per-method documentation pipeline for events. Events (declared in a universal module's logos_events: section) now carry a description parsed from their /// doc comments, and are introspectable at runtime via a new getPluginEvents framework call. - lidl_ast: EventDecl gains a description field. - impl_header_parser: capture the event's doc comment (previously discarded) and an optional metadata.json events[].description. - lidl_gen_provider: generated universal provider emits getEvents() override, mirroring getMethods() (name/signature/ parameters/description; no returnType/isInvokable — events are void). - logos_provider_object: default-empty virtual getEvents() so the legacy provider path and QtProviderObject inherit empty. - module_proxy / qt_provider_object: intercept getPluginEvents next to the getPluginMethods special-case. - docs: spec + README event-documentation notes. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 18 ++++++++ cpp-generator/docs/project.md | 6 +-- cpp-generator/docs/spec.md | 40 ++++++++++++++++- .../experimental/impl_header_parser.cpp | 2 + cpp-generator/experimental/lidl_ast.h | 4 +- .../experimental/lidl_gen_provider.cpp | 44 +++++++++++++++++++ cpp/logos_provider_object.h | 4 ++ cpp/module_proxy.cpp | 12 +++++ cpp/module_proxy.h | 1 + cpp/qt_provider_object.cpp | 6 +++ docs/docs.md | 6 ++- 11 files changed, 137 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 1ee0177..143b9ad 100644 --- a/README.md +++ b/README.md @@ -242,6 +242,24 @@ 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` in the generated +`getEvents()` output, which the framework returns from `getPluginEvents()`, so +it 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 `name`, `signature`, `parameters[]`, and `description` (no +`returnType` — events are fire-and-forget). 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..8422b41 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 into `getEvents()`) - **`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/getEvents dispatch (`getEvents()` emits one entry per `module.events` declaration: name, signature, parameters, and escaped `description`) - `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..67f0839 100644 --- a/cpp-generator/docs/spec.md +++ b/cpp-generator/docs/spec.md @@ -180,6 +180,43 @@ 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 each `getEvents()` +entry. `getEvents()` is what the framework's `getPluginEvents()` returns, so the +description flows — with no extra 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 `getEvents()` gains +`"description": "Emitted once the user has authenticated.\nCarries the freshly issued session token."` + +An event entry carries `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 has no +events and its generated provider inherits an empty `getEvents()`. + +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: @@ -252,10 +289,11 @@ Contains two classes: #### Dispatch (`_dispatch.cpp`) -Implements two methods on the ProviderObject: +Implements three 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. +3. `**getEvents()**` — returns `QJsonArray` of event metadata, one entry per `logos_events:` declaration. Each entry has `name`, `signature`, `parameters[]` (with `type` and `name`), and — when the declaration is preceded by a doc comment — `description` (see [Event documentation](#event-documentation) above). No `returnType`/`isInvokable`: events are void. This array is what the framework's `getPluginEvents()` returns, so events surface in `lm events`, `logoscore module-info`, and Basecamp's Interface screen. The base `LogosProviderObject::getEvents()` returns an empty array, so legacy (`--provider-header`) modules report no events. #### 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..da1842f 100644 --- a/cpp-generator/experimental/lidl_gen_provider.cpp +++ b/cpp-generator/experimental/lidl_gen_provider.cpp @@ -269,6 +269,8 @@ QString lidlMakeProviderHeader(const ModuleDecl& module, s << " LOGOS_PROVIDER(" << providerObjectClass << ", \"" << module.name << "\", \"" << (module.version.isEmpty() ? "0.0.0" : module.version) << "\")\n\n"; s << "public:\n"; + // Event introspection (mirrors getMethods); definition emitted in the dispatch. + s << " QJsonArray getEvents() override;\n"; for (const MethodDecl& md : module.methods) { QString qtRet = lidlTypeToQt(md.returnType); @@ -522,6 +524,48 @@ QString lidlMakeProviderDispatch(const ModuleDecl& module) s << " return methods;\n"; s << "}\n"; + // --- getEvents --- (mirrors getMethods; events are void/fire-and-forget, + // so there is no returnType/isInvokable) + s << "\nQJsonArray " << providerObjectClass << "::getEvents()\n{\n"; + s << " QJsonArray events;\n"; + + for (const EventDecl& ed : module.events) { + s << " {\n"; + s << " QJsonObject obj;\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 << " events.append(obj);\n"; + s << " }\n"; + } + + s << " return events;\n"; + s << "}\n"; + return c; } diff --git a/cpp/logos_provider_object.h b/cpp/logos_provider_object.h index 44f8ce8..aa4d6a5 100644 --- a/cpp/logos_provider_object.h +++ b/cpp/logos_provider_object.h @@ -40,6 +40,10 @@ public: virtual QVariant callMethod(const QString& methodName, const QVariantList& args) = 0; virtual bool informModuleToken(const QString& moduleName, const QString& token) = 0; virtual QJsonArray getMethods() = 0; + // Event introspection — parallels getMethods(). Default empty so legacy Qt + // providers and `provider`-interface modules (which have no logos_events:) + // compile unchanged; universal modules' generated dispatch overrides it. + virtual QJsonArray getEvents() { return QJsonArray(); } virtual void setEventListener(EventCallback callback) = 0; virtual void init(void* apiInstance) = 0; virtual QString providerName() const = 0; diff --git a/cpp/module_proxy.cpp b/cpp/module_proxy.cpp index 2bccc1f..1787f1b 100644 --- a/cpp/module_proxy.cpp +++ b/cpp/module_proxy.cpp @@ -66,6 +66,10 @@ QVariant ModuleProxy::callRemoteMethod(const QString& authToken, const QString& return QVariant(getPluginMethods()); } + if (methodName == "getPluginEvents" && args.isEmpty()) { + return QVariant(getPluginEvents()); + } + qDebug() << "ModuleProxy: callRemoteMethod" << methodName << "args:" << args; return m_provider->callMethod(methodName, args); } @@ -90,4 +94,12 @@ QJsonArray ModuleProxy::getPluginMethods() return m_provider->getMethods(); } +QJsonArray ModuleProxy::getPluginEvents() +{ + if (!m_provider) return QJsonArray(); + + qDebug() << "[LogosProviderObject] ModuleProxy: calling LogosProviderObject::getEvents()"; + return m_provider->getEvents(); +} + #include "moc_module_proxy.cpp" diff --git a/cpp/module_proxy.h b/cpp/module_proxy.h index dcae013..2124ddb 100644 --- a/cpp/module_proxy.h +++ b/cpp/module_proxy.h @@ -31,6 +31,7 @@ public: Q_INVOKABLE bool informModuleToken(const QString& authToken, const QString& moduleName, const QString& token); bool saveToken(const QString& from_module_name, const QString& token); Q_INVOKABLE QJsonArray getPluginMethods(); + Q_INVOKABLE QJsonArray getPluginEvents(); 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..b66c6b9 100644 --- a/cpp/qt_provider_object.cpp +++ b/cpp/qt_provider_object.cpp @@ -261,6 +261,12 @@ QVariant QtProviderObject::callMethod(const QString& methodName, const QVariantL return QVariant(getMethods()); } + // Special-case getPluginEvents (legacy Qt modules have no declared events, + // so getEvents() inherits the empty base default). + if (methodName == "getPluginEvents" && args.isEmpty()) { + return QVariant(getEvents()); + } + // 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..4e726bd 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) - 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,7 @@ 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. | | `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 +481,7 @@ public: const QVariantList& args = {}); bool informModuleToken(const QString& authToken, const QString& moduleName, const QString& token); QJsonArray getPluginMethods(); + QJsonArray getPluginEvents(); signals: void eventResponse(const QString& eventName, const QVariantList& data); @@ -492,10 +495,11 @@ 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 | **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