From a43866013a475a33099310220ad7713e8d16a307 Mon Sep 17 00:00:00 2001 From: GNERSIS Date: Mon, 22 Jun 2026 15:26:24 +0200 Subject: [PATCH 1/4] feat(toolbox): delegated parser-ingest host slots Add create_parser_ingest / release_parser_ingest tail slots to PJ_toolbox_runtime_host_vtable_t so a toolbox plugin can push parsed records into a toolbox-created data source via the standard DataSourceRuntimeHostView (ensureParserBinding / pushMessage). C++ wrappers ToolboxRuntimeHostView::createParserIngest / releaseParserIngest gate on PJ_HAS_TAIL_SLOT and return an "older host" error when the host predates the slot. Tail-appended and struct_size-gated, so no PJ_ABI_VERSION bump; ABI sentinels pin create_parser_ingest@24, release_parser_ingest@32, sizeof==40. Co-Authored-By: Claude Opus 4.8 --- .../pj_base/sdk/toolbox_plugin_base.hpp | 38 +++++++++++++++++++ pj_base/include/pj_base/toolbox_protocol.h | 25 ++++++++++++ pj_base/tests/abi_layout_sentinels_test.cpp | 18 +++++++++ .../pj_plugins/testing/toolbox_test_store.hpp | 2 + pj_plugins/tests/toolbox_plugin_test.cpp | 2 + 5 files changed, 85 insertions(+) diff --git a/pj_base/include/pj_base/sdk/toolbox_plugin_base.hpp b/pj_base/include/pj_base/sdk/toolbox_plugin_base.hpp index b4fd47f1..e832462d 100644 --- a/pj_base/include/pj_base/sdk/toolbox_plugin_base.hpp +++ b/pj_base/include/pj_base/sdk/toolbox_plugin_base.hpp @@ -17,6 +17,7 @@ #include "pj_base/expected.hpp" #include "pj_base/plugin_abi_export.hpp" +#include "pj_base/sdk/data_source_host_views.hpp" // DataSourceRuntimeHostView, errorToString #include "pj_base/sdk/plugin_data_api.hpp" #include "pj_base/sdk/service_registry.hpp" #include "pj_base/sdk/service_traits.hpp" @@ -56,6 +57,43 @@ class ToolboxRuntimeHostView { } } + /// Create (or fetch) the parser-ingest context for a toolbox-created data + /// source (pass ToolboxHostView::createDataSource's handle `.id`). Returns + /// the standard delegated-ingest view: ensureParserBinding() once per topic, + /// pushMessage() per record — exactly like a DataSource plugin. Fails with + /// an "older host" error when the host predates the tail slot. + /// Drive the returned view from a single worker thread (the data-source ingest discipline — see the thread tags in data_source_protocol.h); unlike reportMessage/notifyDataChanged it is NOT GUI-marshalled. + [[nodiscard]] Expected createParserIngest(uint32_t data_source_id) const { + if (!valid()) { + return unexpected("toolbox runtime host is not bound"); + } + if (!PJ_HAS_TAIL_SLOT(PJ_toolbox_runtime_host_vtable_t, host_.vtable, create_parser_ingest)) { + return unexpected("toolbox runtime host does not support create_parser_ingest (older host)"); + } + PJ_data_source_runtime_host_t raw{}; + PJ_error_t err{}; + if (!host_.vtable->create_parser_ingest(host_.ctx, data_source_id, &raw, &err)) { + return unexpected(errorToString(err)); + } + return DataSourceRuntimeHostView{raw}; + } + + /// Flush + destroy the context. Idempotent. The view returned by + /// createParserIngest must not be used afterwards. + [[nodiscard]] Status releaseParserIngest(uint32_t data_source_id) const { + if (!valid()) { + return unexpected("toolbox runtime host is not bound"); + } + if (!PJ_HAS_TAIL_SLOT(PJ_toolbox_runtime_host_vtable_t, host_.vtable, release_parser_ingest)) { + return unexpected("toolbox runtime host does not support release_parser_ingest (older host)"); + } + PJ_error_t err{}; + if (!host_.vtable->release_parser_ingest(host_.ctx, data_source_id, &err)) { + return unexpected(errorToString(err)); + } + return okStatus(); + } + [[nodiscard]] const PJ_toolbox_runtime_host_t& raw() const { return host_; } diff --git a/pj_base/include/pj_base/toolbox_protocol.h b/pj_base/include/pj_base/toolbox_protocol.h index e61bb93e..d3dac249 100644 --- a/pj_base/include/pj_base/toolbox_protocol.h +++ b/pj_base/include/pj_base/toolbox_protocol.h @@ -19,6 +19,7 @@ #include #include +#include "pj_base/data_source_protocol.h" /* PJ_data_source_runtime_host_t for parser ingest */ #include "pj_base/plugin_data_api.h" #ifdef __cplusplus @@ -74,6 +75,30 @@ typedef struct PJ_toolbox_runtime_host_vtable_t { /** [thread-safe] Notify the host that data has been modified; host refreshes UI. */ void (*notify_data_changed)(void* ctx) PJ_NOEXCEPT; + + /* ---- TAIL SLOTS (parser ingest) ------------------------------------ + * Appended for toolbox-delegated parsing; struct_size-gated via + * PJ_HAS_TAIL_SLOT, no protocol_version bump — the same growth mechanism + * as the toolbox write host's object-topic slots (ABI v5). */ + + /** [thread-safe] Create (or return the existing) parser-ingest context bound to a + * toolbox-created data source. `data_source_id` is the handle id returned + * by the toolbox write host's create_data_source (== the dataset id). + * On success fills `out_host` with a standard data-source runtime host + * fat pointer: ensure_parser_binding / push_message on it behave exactly + * as they do for file/stream DataSource plugins. The context stays valid + * until release_parser_ingest or host teardown. + * Returns false (with out_error populated) if `data_source_id` is not a live + * data source or parser ingest is not configured on this host; `out_host` is + * left untouched on failure. */ + bool (*create_parser_ingest)( + void* ctx, uint32_t data_source_id, PJ_data_source_runtime_host_t* out_host, + PJ_error_t* out_error) PJ_NOEXCEPT; + + /** [thread-safe] Flush every row written through the context's parser bindings and + * destroy it. Idempotent: releasing an unknown id succeeds. The fat + * pointer from create_parser_ingest must not be used afterwards. */ + bool (*release_parser_ingest)(void* ctx, uint32_t data_source_id, PJ_error_t* out_error) PJ_NOEXCEPT; } PJ_toolbox_runtime_host_vtable_t; typedef struct { diff --git a/pj_base/tests/abi_layout_sentinels_test.cpp b/pj_base/tests/abi_layout_sentinels_test.cpp index 87a6553d..77e75903 100644 --- a/pj_base/tests/abi_layout_sentinels_test.cpp +++ b/pj_base/tests/abi_layout_sentinels_test.cpp @@ -180,6 +180,24 @@ static_assert(offsetof(PJ_toolbox_host_vtable_t, register_object_topic) == 72, " static_assert(offsetof(PJ_toolbox_host_vtable_t, push_owned_object) == 80, "toolbox host object-push tail slot pinned"); static_assert(sizeof(PJ_toolbox_host_vtable_t) == 88, "Toolbox host size (update deliberately on append)"); +// --- Toolbox runtime host vtable (ABI-APPENDABLE within v4) ------------------ +// The vtable the host exposes to plugins under "pj.toolbox_runtime.v1". +// Offsets of existing slots are pinned; size grows deliberately as tail slots append. +static_assert(offsetof(PJ_toolbox_runtime_host_vtable_t, protocol_version) == 0, "toolbox runtime v1 prefix pinned"); +static_assert(offsetof(PJ_toolbox_runtime_host_vtable_t, struct_size) == 4, "toolbox runtime v1 prefix pinned"); +static_assert(offsetof(PJ_toolbox_runtime_host_vtable_t, report_message) == 8, "toolbox runtime first slot pinned"); +static_assert( + offsetof(PJ_toolbox_runtime_host_vtable_t, notify_data_changed) == 16, + "toolbox runtime second slot pinned"); +static_assert( + offsetof(PJ_toolbox_runtime_host_vtable_t, create_parser_ingest) == 24, + "toolbox runtime parser-ingest slot pinned"); +static_assert( + offsetof(PJ_toolbox_runtime_host_vtable_t, release_parser_ingest) == 32, + "toolbox runtime parser-ingest release slot pinned"); +static_assert( + sizeof(PJ_toolbox_runtime_host_vtable_t) == 40, "Toolbox runtime host size (update deliberately on append)"); + // --- ABI version symbol ------------------------------------------------------ static_assert(PJ_ABI_VERSION == 5, "v5 ABI version"); diff --git a/pj_plugins/include/pj_plugins/testing/toolbox_test_store.hpp b/pj_plugins/include/pj_plugins/testing/toolbox_test_store.hpp index 67d59a10..81ac07c3 100644 --- a/pj_plugins/include/pj_plugins/testing/toolbox_test_store.hpp +++ b/pj_plugins/include/pj_plugins/testing/toolbox_test_store.hpp @@ -157,6 +157,8 @@ class ToolboxTestStore { .struct_size = sizeof(PJ_toolbox_runtime_host_vtable_t), .report_message = &ToolboxTestStore::trampolineReportMessage, .notify_data_changed = &ToolboxTestStore::trampolineNotifyDataChanged, + .create_parser_ingest = nullptr, + .release_parser_ingest = nullptr, }; return PJ_toolbox_runtime_host_t{.ctx = this, .vtable = &vtable}; } diff --git a/pj_plugins/tests/toolbox_plugin_test.cpp b/pj_plugins/tests/toolbox_plugin_test.cpp index d3b71421..00855e30 100644 --- a/pj_plugins/tests/toolbox_plugin_test.cpp +++ b/pj_plugins/tests/toolbox_plugin_test.cpp @@ -103,6 +103,8 @@ PJ_toolbox_runtime_host_t makeRuntimeHost(RuntimeState* state) { .struct_size = sizeof(PJ_toolbox_runtime_host_vtable_t), .report_message = rhReportMessage, .notify_data_changed = rhNotifyDataChanged, + .create_parser_ingest = nullptr, /* tail slot — not implemented in test stub */ + .release_parser_ingest = nullptr, /* tail slot — not implemented in test stub */ }; return PJ_toolbox_runtime_host_t{.ctx = state, .vtable = &vtable}; } From 13bb38a227341d1bf4f708efd06f22b14e045513 Mon Sep 17 00:00:00 2001 From: GNERSIS Date: Mon, 22 Jun 2026 15:26:24 +0200 Subject: [PATCH 2/4] feat(dialog): RangeSlider markers (setRangeSliderMarkers / rangeSliderMarkers) WidgetData::setRangeSliderMarkers emits a "range_markers" array of {start,end,label}; WidgetDataView::rangeSliderMarkers reads it back (nullopt = never set, empty = cleared). Each marker draws a box at its true [start,end] extent; the host shades boxes overlapping the current [lower,upper] selection. Co-Authored-By: Claude Opus 4.8 --- .../pj_plugins/host/widget_data_view.hpp | 37 +++++++++++++++++++ .../include/pj_plugins/sdk/widget_data.hpp | 27 ++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/pj_plugins/dialog_protocol/include/pj_plugins/host/widget_data_view.hpp b/pj_plugins/dialog_protocol/include/pj_plugins/host/widget_data_view.hpp index 4037eccf..82cfd062 100644 --- a/pj_plugins/dialog_protocol/include/pj_plugins/host/widget_data_view.hpp +++ b/pj_plugins/dialog_protocol/include/pj_plugins/host/widget_data_view.hpp @@ -306,6 +306,43 @@ class WidgetDataView { } } + /// Boundary segments for a RangeSlider: (start, end, label) in slider units. + /// nullopt when never set; an empty vector when explicitly cleared. + struct Marker { + int start = 0; + int end = 0; + std::string label; + }; + [[nodiscard]] std::optional> rangeSliderMarkers(std::string_view name) const { + const nlohmann::json* w = widget(name); + if (!w) { + return std::nullopt; + } + auto it = w->find("range_markers"); + if (it == w->end() || !it->is_array()) { + return std::nullopt; + } + std::vector out; + out.reserve(it->size()); + for (const auto& m : *it) { + if (!m.is_object()) { + continue; + } + auto s = m.find("start"); + auto e = m.find("end"); + auto l = m.find("label"); + if (s == m.end() || !s->is_number_integer()) { + continue; + } + Marker mk; + mk.start = s->get(); + mk.end = (e != m.end() && e->is_number_integer()) ? e->get() : mk.start; + mk.label = (l != m.end() && l->is_string()) ? l->get() : std::string{}; + out.push_back(std::move(mk)); + } + return out; + } + // --- DateRangePicker --- [[nodiscard]] std::optional dateRangeEarliest(std::string_view name) const { return getString(name, "date_range_earliest"); diff --git a/pj_plugins/dialog_protocol/include/pj_plugins/sdk/widget_data.hpp b/pj_plugins/dialog_protocol/include/pj_plugins/sdk/widget_data.hpp index 4db30cb0..4dee6f62 100644 --- a/pj_plugins/dialog_protocol/include/pj_plugins/sdk/widget_data.hpp +++ b/pj_plugins/dialog_protocol/include/pj_plugins/sdk/widget_data.hpp @@ -26,6 +26,17 @@ struct ChartSeries { std::string color; // optional hex "#rrggbb" }; +/// One boundary segment on a RangeSlider (used by setRangeSliderMarkers): a box +/// covering [start, end] in slider units, with an optional label. The host draws +/// each as a distinct box at its true extent — so disjoint selections leave +/// blank slider space in the gaps — and shades the boxes overlapping the current +/// [lower, upper] selection. +struct RangeSliderMarker { + int start = 0; + int end = 0; + std::string label; +}; + /// Builder for the JSON string returned by get_widget_data(). /// Each method targets an existing widget in the .ui file by its objectName. class WidgetData { @@ -341,6 +352,22 @@ class WidgetData { return *this; } + /// Draw boundary segments on a RangeSlider: one box per marker covering its + /// [start, end] (in slider units, same space as the handles) with an optional + /// label centered inside. Each box is drawn at its TRUE extent, so a disjoint + /// selection leaves blank slider space between boxes; the host shades the + /// boxes overlapping the current [lower, upper] selection, so the slider + /// doubles as a "which segment falls in the range" indicator. Empty clears. + WidgetData& setRangeSliderMarkers(std::string_view name, const std::vector& markers) { + auto& e = entry(name); + nlohmann::json arr = nlohmann::json::array(); + for (const auto& m : markers) { + arr.push_back(nlohmann::json{{"start", m.start}, {"end", m.end}, {"label", m.label}}); + } + e["range_markers"] = std::move(arr); + return *this; + } + // --- Field validity indicator (generic) --- /// Mark a field's value valid/invalid for a small inline indicator (e.g. a From aa8ab6ba53ef56c8ce750e3f7f114044e89d1d86 Mon Sep 17 00:00:00 2001 From: Davide Faconti Date: Mon, 22 Jun 2026 16:01:06 +0200 Subject: [PATCH 3/4] refactor(parser-ingest): narrow shared ingest interface --- .../pj_base/sdk/data_source_host_views.hpp | 32 +++++++++++++++++++ .../pj_base/sdk/toolbox_plugin_base.hpp | 14 ++++---- pj_base/tests/push_message_test.cpp | 6 ++-- 3 files changed, 42 insertions(+), 10 deletions(-) diff --git a/pj_base/include/pj_base/sdk/data_source_host_views.hpp b/pj_base/include/pj_base/sdk/data_source_host_views.hpp index e5b26c09..848e0d2e 100644 --- a/pj_base/include/pj_base/sdk/data_source_host_views.hpp +++ b/pj_base/include/pj_base/sdk/data_source_host_views.hpp @@ -112,6 +112,8 @@ struct ParserBindingRequest { return out; } +class ParserIngestHostView; + /** * Type-safe view over the runtime host vtable. * @@ -129,6 +131,8 @@ class DataSourceRuntimeHostView { return host_.ctx != nullptr && host_.vtable != nullptr; } + [[nodiscard]] ParserIngestHostView parserIngest() const noexcept; + /// Send a diagnostic message to the host UI log. Never fails. void reportMessage(DataSourceMessageLevel level, std::string_view message) const { if (valid() && host_.vtable->report_message != nullptr) { @@ -362,4 +366,32 @@ class DataSourceRuntimeHostView { PJ_data_source_runtime_host_t host_{}; }; +/// Narrow delegated-ingest facade shared by DataSource and Toolbox code. +class ParserIngestHostView { + public: + ParserIngestHostView() = default; + explicit ParserIngestHostView(PJ_data_source_runtime_host_t host) : host_(host) {} + + [[nodiscard]] bool valid() const noexcept { + return host_.valid(); + } + + [[nodiscard]] Expected ensureParserBinding(const ParserBindingRequest& request) const { + return host_.ensureParserBinding(request); + } + + template + [[nodiscard]] Status pushMessage( + ParserBindingHandle handle, Timestamp host_timestamp_ns, FetchMessageData&& fetch_message_data) const { + return host_.pushMessage(handle, host_timestamp_ns, std::forward(fetch_message_data)); + } + + private: + DataSourceRuntimeHostView host_{}; +}; + +inline ParserIngestHostView DataSourceRuntimeHostView::parserIngest() const noexcept { + return ParserIngestHostView{host_}; +} + } // namespace PJ diff --git a/pj_base/include/pj_base/sdk/toolbox_plugin_base.hpp b/pj_base/include/pj_base/sdk/toolbox_plugin_base.hpp index e832462d..3cc6849f 100644 --- a/pj_base/include/pj_base/sdk/toolbox_plugin_base.hpp +++ b/pj_base/include/pj_base/sdk/toolbox_plugin_base.hpp @@ -17,7 +17,7 @@ #include "pj_base/expected.hpp" #include "pj_base/plugin_abi_export.hpp" -#include "pj_base/sdk/data_source_host_views.hpp" // DataSourceRuntimeHostView, errorToString +#include "pj_base/sdk/data_source_host_views.hpp" // ParserIngestHostView, errorToString #include "pj_base/sdk/plugin_data_api.hpp" #include "pj_base/sdk/service_registry.hpp" #include "pj_base/sdk/service_traits.hpp" @@ -59,11 +59,11 @@ class ToolboxRuntimeHostView { /// Create (or fetch) the parser-ingest context for a toolbox-created data /// source (pass ToolboxHostView::createDataSource's handle `.id`). Returns - /// the standard delegated-ingest view: ensureParserBinding() once per topic, - /// pushMessage() per record — exactly like a DataSource plugin. Fails with - /// an "older host" error when the host predates the tail slot. - /// Drive the returned view from a single worker thread (the data-source ingest discipline — see the thread tags in data_source_protocol.h); unlike reportMessage/notifyDataChanged it is NOT GUI-marshalled. - [[nodiscard]] Expected createParserIngest(uint32_t data_source_id) const { + /// the shared delegated-ingest view: ensureParserBinding() once per topic, + /// pushMessage() per record. Drive the returned view from a single worker + /// thread; unlike reportMessage/notifyDataChanged, parser ingest is not + /// GUI-marshalled. + [[nodiscard]] Expected createParserIngest(uint32_t data_source_id) const { if (!valid()) { return unexpected("toolbox runtime host is not bound"); } @@ -75,7 +75,7 @@ class ToolboxRuntimeHostView { if (!host_.vtable->create_parser_ingest(host_.ctx, data_source_id, &raw, &err)) { return unexpected(errorToString(err)); } - return DataSourceRuntimeHostView{raw}; + return ParserIngestHostView{raw}; } /// Flush + destroy the context. Idempotent. The view returned by diff --git a/pj_base/tests/push_message_test.cpp b/pj_base/tests/push_message_test.cpp index ce8316f0..137ee4da 100644 --- a/pj_base/tests/push_message_test.cpp +++ b/pj_base/tests/push_message_test.cpp @@ -1,7 +1,7 @@ // Copyright 2026 Davide Faconti // SPDX-License-Identifier: Apache-2.0 -// Tests for the SDK template `DataSourceRuntimeHostView::pushMessage` and +// Tests for the SDK template `ParserIngestHostView::pushMessage` and // its delegation to the C ABI slot `push_message`. We exercise: // // 1. Vector closure → the captured FetchMessageData callable yields the @@ -54,8 +54,8 @@ class MockHost { vtable_.struct_size = offsetof(PJ_data_source_runtime_host_vtable_t, push_message); } - PJ::DataSourceRuntimeHostView view() const { - return PJ::DataSourceRuntimeHostView(host_); + PJ::ParserIngestHostView view() const { + return PJ::DataSourceRuntimeHostView(host_).parserIngest(); } CapturedPush& captured() { From da4c0b0c2b926c44bb9f91284251022f14f1becc Mon Sep 17 00:00:00 2001 From: Davide Faconti Date: Mon, 22 Jun 2026 16:20:48 +0200 Subject: [PATCH 4/4] fix formatting --- pj_base/include/pj_base/toolbox_protocol.h | 3 +-- pj_base/tests/abi_layout_sentinels_test.cpp | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/pj_base/include/pj_base/toolbox_protocol.h b/pj_base/include/pj_base/toolbox_protocol.h index d3dac249..7e12fc62 100644 --- a/pj_base/include/pj_base/toolbox_protocol.h +++ b/pj_base/include/pj_base/toolbox_protocol.h @@ -92,8 +92,7 @@ typedef struct PJ_toolbox_runtime_host_vtable_t { * data source or parser ingest is not configured on this host; `out_host` is * left untouched on failure. */ bool (*create_parser_ingest)( - void* ctx, uint32_t data_source_id, PJ_data_source_runtime_host_t* out_host, - PJ_error_t* out_error) PJ_NOEXCEPT; + void* ctx, uint32_t data_source_id, PJ_data_source_runtime_host_t* out_host, PJ_error_t* out_error) PJ_NOEXCEPT; /** [thread-safe] Flush every row written through the context's parser bindings and * destroy it. Idempotent: releasing an unknown id succeeds. The fat diff --git a/pj_base/tests/abi_layout_sentinels_test.cpp b/pj_base/tests/abi_layout_sentinels_test.cpp index 77e75903..1a1466ea 100644 --- a/pj_base/tests/abi_layout_sentinels_test.cpp +++ b/pj_base/tests/abi_layout_sentinels_test.cpp @@ -187,8 +187,7 @@ static_assert(offsetof(PJ_toolbox_runtime_host_vtable_t, protocol_version) == 0, static_assert(offsetof(PJ_toolbox_runtime_host_vtable_t, struct_size) == 4, "toolbox runtime v1 prefix pinned"); static_assert(offsetof(PJ_toolbox_runtime_host_vtable_t, report_message) == 8, "toolbox runtime first slot pinned"); static_assert( - offsetof(PJ_toolbox_runtime_host_vtable_t, notify_data_changed) == 16, - "toolbox runtime second slot pinned"); + offsetof(PJ_toolbox_runtime_host_vtable_t, notify_data_changed) == 16, "toolbox runtime second slot pinned"); static_assert( offsetof(PJ_toolbox_runtime_host_vtable_t, create_parser_ingest) == 24, "toolbox runtime parser-ingest slot pinned");