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 b4fd47f1..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,6 +17,7 @@ #include "pj_base/expected.hpp" #include "pj_base/plugin_abi_export.hpp" +#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" @@ -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 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"); + } + 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 ParserIngestHostView{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..7e12fc62 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,29 @@ 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..1a1466ea 100644 --- a/pj_base/tests/abi_layout_sentinels_test.cpp +++ b/pj_base/tests/abi_layout_sentinels_test.cpp @@ -180,6 +180,23 @@ 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_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() { 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 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}; }