Skip to content

Commit 7975095

Browse files
facontidavideGNERSISclaude
authored
Feat/toolbox parser ingest rangeslider markers (#133)
* 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * refactor(parser-ingest): narrow shared ingest interface * fix formatting --------- Co-authored-by: GNERSIS <gornersisyan4@gmail.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 6e91268 commit 7975095

9 files changed

Lines changed: 182 additions & 3 deletions

File tree

pj_base/include/pj_base/sdk/data_source_host_views.hpp

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,8 @@ struct ParserBindingRequest {
112112
return out;
113113
}
114114

115+
class ParserIngestHostView;
116+
115117
/**
116118
* Type-safe view over the runtime host vtable.
117119
*
@@ -129,6 +131,8 @@ class DataSourceRuntimeHostView {
129131
return host_.ctx != nullptr && host_.vtable != nullptr;
130132
}
131133

134+
[[nodiscard]] ParserIngestHostView parserIngest() const noexcept;
135+
132136
/// Send a diagnostic message to the host UI log. Never fails.
133137
void reportMessage(DataSourceMessageLevel level, std::string_view message) const {
134138
if (valid() && host_.vtable->report_message != nullptr) {
@@ -362,4 +366,32 @@ class DataSourceRuntimeHostView {
362366
PJ_data_source_runtime_host_t host_{};
363367
};
364368

369+
/// Narrow delegated-ingest facade shared by DataSource and Toolbox code.
370+
class ParserIngestHostView {
371+
public:
372+
ParserIngestHostView() = default;
373+
explicit ParserIngestHostView(PJ_data_source_runtime_host_t host) : host_(host) {}
374+
375+
[[nodiscard]] bool valid() const noexcept {
376+
return host_.valid();
377+
}
378+
379+
[[nodiscard]] Expected<ParserBindingHandle> ensureParserBinding(const ParserBindingRequest& request) const {
380+
return host_.ensureParserBinding(request);
381+
}
382+
383+
template <typename FetchMessageData>
384+
[[nodiscard]] Status pushMessage(
385+
ParserBindingHandle handle, Timestamp host_timestamp_ns, FetchMessageData&& fetch_message_data) const {
386+
return host_.pushMessage(handle, host_timestamp_ns, std::forward<FetchMessageData>(fetch_message_data));
387+
}
388+
389+
private:
390+
DataSourceRuntimeHostView host_{};
391+
};
392+
393+
inline ParserIngestHostView DataSourceRuntimeHostView::parserIngest() const noexcept {
394+
return ParserIngestHostView{host_};
395+
}
396+
365397
} // namespace PJ

pj_base/include/pj_base/sdk/toolbox_plugin_base.hpp

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
#include "pj_base/expected.hpp"
1919
#include "pj_base/plugin_abi_export.hpp"
20+
#include "pj_base/sdk/data_source_host_views.hpp" // ParserIngestHostView, errorToString
2021
#include "pj_base/sdk/plugin_data_api.hpp"
2122
#include "pj_base/sdk/service_registry.hpp"
2223
#include "pj_base/sdk/service_traits.hpp"
@@ -56,6 +57,43 @@ class ToolboxRuntimeHostView {
5657
}
5758
}
5859

60+
/// Create (or fetch) the parser-ingest context for a toolbox-created data
61+
/// source (pass ToolboxHostView::createDataSource's handle `.id`). Returns
62+
/// the shared delegated-ingest view: ensureParserBinding() once per topic,
63+
/// pushMessage() per record. Drive the returned view from a single worker
64+
/// thread; unlike reportMessage/notifyDataChanged, parser ingest is not
65+
/// GUI-marshalled.
66+
[[nodiscard]] Expected<ParserIngestHostView> createParserIngest(uint32_t data_source_id) const {
67+
if (!valid()) {
68+
return unexpected("toolbox runtime host is not bound");
69+
}
70+
if (!PJ_HAS_TAIL_SLOT(PJ_toolbox_runtime_host_vtable_t, host_.vtable, create_parser_ingest)) {
71+
return unexpected("toolbox runtime host does not support create_parser_ingest (older host)");
72+
}
73+
PJ_data_source_runtime_host_t raw{};
74+
PJ_error_t err{};
75+
if (!host_.vtable->create_parser_ingest(host_.ctx, data_source_id, &raw, &err)) {
76+
return unexpected(errorToString(err));
77+
}
78+
return ParserIngestHostView{raw};
79+
}
80+
81+
/// Flush + destroy the context. Idempotent. The view returned by
82+
/// createParserIngest must not be used afterwards.
83+
[[nodiscard]] Status releaseParserIngest(uint32_t data_source_id) const {
84+
if (!valid()) {
85+
return unexpected("toolbox runtime host is not bound");
86+
}
87+
if (!PJ_HAS_TAIL_SLOT(PJ_toolbox_runtime_host_vtable_t, host_.vtable, release_parser_ingest)) {
88+
return unexpected("toolbox runtime host does not support release_parser_ingest (older host)");
89+
}
90+
PJ_error_t err{};
91+
if (!host_.vtable->release_parser_ingest(host_.ctx, data_source_id, &err)) {
92+
return unexpected(errorToString(err));
93+
}
94+
return okStatus();
95+
}
96+
5997
[[nodiscard]] const PJ_toolbox_runtime_host_t& raw() const {
6098
return host_;
6199
}

pj_base/include/pj_base/toolbox_protocol.h

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
#include <stddef.h>
2020
#include <stdint.h>
2121

22+
#include "pj_base/data_source_protocol.h" /* PJ_data_source_runtime_host_t for parser ingest */
2223
#include "pj_base/plugin_data_api.h"
2324

2425
#ifdef __cplusplus
@@ -74,6 +75,29 @@ typedef struct PJ_toolbox_runtime_host_vtable_t {
7475

7576
/** [thread-safe] Notify the host that data has been modified; host refreshes UI. */
7677
void (*notify_data_changed)(void* ctx) PJ_NOEXCEPT;
78+
79+
/* ---- TAIL SLOTS (parser ingest) ------------------------------------
80+
* Appended for toolbox-delegated parsing; struct_size-gated via
81+
* PJ_HAS_TAIL_SLOT, no protocol_version bump — the same growth mechanism
82+
* as the toolbox write host's object-topic slots (ABI v5). */
83+
84+
/** [thread-safe] Create (or return the existing) parser-ingest context bound to a
85+
* toolbox-created data source. `data_source_id` is the handle id returned
86+
* by the toolbox write host's create_data_source (== the dataset id).
87+
* On success fills `out_host` with a standard data-source runtime host
88+
* fat pointer: ensure_parser_binding / push_message on it behave exactly
89+
* as they do for file/stream DataSource plugins. The context stays valid
90+
* until release_parser_ingest or host teardown.
91+
* Returns false (with out_error populated) if `data_source_id` is not a live
92+
* data source or parser ingest is not configured on this host; `out_host` is
93+
* left untouched on failure. */
94+
bool (*create_parser_ingest)(
95+
void* ctx, uint32_t data_source_id, PJ_data_source_runtime_host_t* out_host, PJ_error_t* out_error) PJ_NOEXCEPT;
96+
97+
/** [thread-safe] Flush every row written through the context's parser bindings and
98+
* destroy it. Idempotent: releasing an unknown id succeeds. The fat
99+
* pointer from create_parser_ingest must not be used afterwards. */
100+
bool (*release_parser_ingest)(void* ctx, uint32_t data_source_id, PJ_error_t* out_error) PJ_NOEXCEPT;
77101
} PJ_toolbox_runtime_host_vtable_t;
78102

79103
typedef struct {

pj_base/tests/abi_layout_sentinels_test.cpp

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,23 @@ static_assert(offsetof(PJ_toolbox_host_vtable_t, register_object_topic) == 72, "
180180
static_assert(offsetof(PJ_toolbox_host_vtable_t, push_owned_object) == 80, "toolbox host object-push tail slot pinned");
181181
static_assert(sizeof(PJ_toolbox_host_vtable_t) == 88, "Toolbox host size (update deliberately on append)");
182182

183+
// --- Toolbox runtime host vtable (ABI-APPENDABLE within v4) ------------------
184+
// The vtable the host exposes to plugins under "pj.toolbox_runtime.v1".
185+
// Offsets of existing slots are pinned; size grows deliberately as tail slots append.
186+
static_assert(offsetof(PJ_toolbox_runtime_host_vtable_t, protocol_version) == 0, "toolbox runtime v1 prefix pinned");
187+
static_assert(offsetof(PJ_toolbox_runtime_host_vtable_t, struct_size) == 4, "toolbox runtime v1 prefix pinned");
188+
static_assert(offsetof(PJ_toolbox_runtime_host_vtable_t, report_message) == 8, "toolbox runtime first slot pinned");
189+
static_assert(
190+
offsetof(PJ_toolbox_runtime_host_vtable_t, notify_data_changed) == 16, "toolbox runtime second slot pinned");
191+
static_assert(
192+
offsetof(PJ_toolbox_runtime_host_vtable_t, create_parser_ingest) == 24,
193+
"toolbox runtime parser-ingest slot pinned");
194+
static_assert(
195+
offsetof(PJ_toolbox_runtime_host_vtable_t, release_parser_ingest) == 32,
196+
"toolbox runtime parser-ingest release slot pinned");
197+
static_assert(
198+
sizeof(PJ_toolbox_runtime_host_vtable_t) == 40, "Toolbox runtime host size (update deliberately on append)");
199+
183200
// --- ABI version symbol ------------------------------------------------------
184201
static_assert(PJ_ABI_VERSION == 5, "v5 ABI version");
185202

pj_base/tests/push_message_test.cpp

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright 2026 Davide Faconti
22
// SPDX-License-Identifier: Apache-2.0
33

4-
// Tests for the SDK template `DataSourceRuntimeHostView::pushMessage` and
4+
// Tests for the SDK template `ParserIngestHostView::pushMessage` and
55
// its delegation to the C ABI slot `push_message`. We exercise:
66
//
77
// 1. Vector closure → the captured FetchMessageData callable yields the
@@ -54,8 +54,8 @@ class MockHost {
5454
vtable_.struct_size = offsetof(PJ_data_source_runtime_host_vtable_t, push_message);
5555
}
5656

57-
PJ::DataSourceRuntimeHostView view() const {
58-
return PJ::DataSourceRuntimeHostView(host_);
57+
PJ::ParserIngestHostView view() const {
58+
return PJ::DataSourceRuntimeHostView(host_).parserIngest();
5959
}
6060

6161
CapturedPush& captured() {

pj_plugins/dialog_protocol/include/pj_plugins/host/widget_data_view.hpp

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,43 @@ class WidgetDataView {
306306
}
307307
}
308308

309+
/// Boundary segments for a RangeSlider: (start, end, label) in slider units.
310+
/// nullopt when never set; an empty vector when explicitly cleared.
311+
struct Marker {
312+
int start = 0;
313+
int end = 0;
314+
std::string label;
315+
};
316+
[[nodiscard]] std::optional<std::vector<Marker>> rangeSliderMarkers(std::string_view name) const {
317+
const nlohmann::json* w = widget(name);
318+
if (!w) {
319+
return std::nullopt;
320+
}
321+
auto it = w->find("range_markers");
322+
if (it == w->end() || !it->is_array()) {
323+
return std::nullopt;
324+
}
325+
std::vector<Marker> out;
326+
out.reserve(it->size());
327+
for (const auto& m : *it) {
328+
if (!m.is_object()) {
329+
continue;
330+
}
331+
auto s = m.find("start");
332+
auto e = m.find("end");
333+
auto l = m.find("label");
334+
if (s == m.end() || !s->is_number_integer()) {
335+
continue;
336+
}
337+
Marker mk;
338+
mk.start = s->get<int>();
339+
mk.end = (e != m.end() && e->is_number_integer()) ? e->get<int>() : mk.start;
340+
mk.label = (l != m.end() && l->is_string()) ? l->get<std::string>() : std::string{};
341+
out.push_back(std::move(mk));
342+
}
343+
return out;
344+
}
345+
309346
// --- DateRangePicker ---
310347
[[nodiscard]] std::optional<std::string> dateRangeEarliest(std::string_view name) const {
311348
return getString(name, "date_range_earliest");

pj_plugins/dialog_protocol/include/pj_plugins/sdk/widget_data.hpp

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,17 @@ struct ChartSeries {
2626
std::string color; // optional hex "#rrggbb"
2727
};
2828

29+
/// One boundary segment on a RangeSlider (used by setRangeSliderMarkers): a box
30+
/// covering [start, end] in slider units, with an optional label. The host draws
31+
/// each as a distinct box at its true extent — so disjoint selections leave
32+
/// blank slider space in the gaps — and shades the boxes overlapping the current
33+
/// [lower, upper] selection.
34+
struct RangeSliderMarker {
35+
int start = 0;
36+
int end = 0;
37+
std::string label;
38+
};
39+
2940
/// Builder for the JSON string returned by get_widget_data().
3041
/// Each method targets an existing widget in the .ui file by its objectName.
3142
class WidgetData {
@@ -341,6 +352,22 @@ class WidgetData {
341352
return *this;
342353
}
343354

355+
/// Draw boundary segments on a RangeSlider: one box per marker covering its
356+
/// [start, end] (in slider units, same space as the handles) with an optional
357+
/// label centered inside. Each box is drawn at its TRUE extent, so a disjoint
358+
/// selection leaves blank slider space between boxes; the host shades the
359+
/// boxes overlapping the current [lower, upper] selection, so the slider
360+
/// doubles as a "which segment falls in the range" indicator. Empty clears.
361+
WidgetData& setRangeSliderMarkers(std::string_view name, const std::vector<RangeSliderMarker>& markers) {
362+
auto& e = entry(name);
363+
nlohmann::json arr = nlohmann::json::array();
364+
for (const auto& m : markers) {
365+
arr.push_back(nlohmann::json{{"start", m.start}, {"end", m.end}, {"label", m.label}});
366+
}
367+
e["range_markers"] = std::move(arr);
368+
return *this;
369+
}
370+
344371
// --- Field validity indicator (generic) ---
345372

346373
/// Mark a field's value valid/invalid for a small inline indicator (e.g. a

pj_plugins/include/pj_plugins/testing/toolbox_test_store.hpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,8 @@ class ToolboxTestStore {
157157
.struct_size = sizeof(PJ_toolbox_runtime_host_vtable_t),
158158
.report_message = &ToolboxTestStore::trampolineReportMessage,
159159
.notify_data_changed = &ToolboxTestStore::trampolineNotifyDataChanged,
160+
.create_parser_ingest = nullptr,
161+
.release_parser_ingest = nullptr,
160162
};
161163
return PJ_toolbox_runtime_host_t{.ctx = this, .vtable = &vtable};
162164
}

pj_plugins/tests/toolbox_plugin_test.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,8 @@ PJ_toolbox_runtime_host_t makeRuntimeHost(RuntimeState* state) {
103103
.struct_size = sizeof(PJ_toolbox_runtime_host_vtable_t),
104104
.report_message = rhReportMessage,
105105
.notify_data_changed = rhNotifyDataChanged,
106+
.create_parser_ingest = nullptr, /* tail slot — not implemented in test stub */
107+
.release_parser_ingest = nullptr, /* tail slot — not implemented in test stub */
106108
};
107109
return PJ_toolbox_runtime_host_t{.ctx = state, .vtable = &vtable};
108110
}

0 commit comments

Comments
 (0)