Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions pj_base/include/pj_base/sdk/data_source_host_views.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@ struct ParserBindingRequest {
return out;
}

class ParserIngestHostView;

/**
* Type-safe view over the runtime host vtable.
*
Expand All @@ -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) {
Expand Down Expand Up @@ -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<ParserBindingHandle> ensureParserBinding(const ParserBindingRequest& request) const {
return host_.ensureParserBinding(request);
}

template <typename FetchMessageData>
[[nodiscard]] Status pushMessage(
ParserBindingHandle handle, Timestamp host_timestamp_ns, FetchMessageData&& fetch_message_data) const {
return host_.pushMessage(handle, host_timestamp_ns, std::forward<FetchMessageData>(fetch_message_data));
}

private:
DataSourceRuntimeHostView host_{};
};

inline ParserIngestHostView DataSourceRuntimeHostView::parserIngest() const noexcept {
return ParserIngestHostView{host_};
}

} // namespace PJ
38 changes: 38 additions & 0 deletions pj_base/include/pj_base/sdk/toolbox_plugin_base.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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<ParserIngestHostView> 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_;
}
Expand Down
24 changes: 24 additions & 0 deletions pj_base/include/pj_base/toolbox_protocol.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
#include <stddef.h>
#include <stdint.h>

#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
Expand Down Expand Up @@ -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 {
Expand Down
17 changes: 17 additions & 0 deletions pj_base/tests/abi_layout_sentinels_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Expand Down
6 changes: 3 additions & 3 deletions pj_base/tests/push_message_test.cpp
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::vector<Marker>> 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<Marker> 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<int>();
mk.end = (e != m.end() && e->is_number_integer()) ? e->get<int>() : mk.start;
mk.label = (l != m.end() && l->is_string()) ? l->get<std::string>() : std::string{};
out.push_back(std::move(mk));
}
return out;
}

// --- DateRangePicker ---
[[nodiscard]] std::optional<std::string> dateRangeEarliest(std::string_view name) const {
return getString(name, "date_range_earliest");
Expand Down
27 changes: 27 additions & 0 deletions pj_plugins/dialog_protocol/include/pj_plugins/sdk/widget_data.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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<RangeSliderMarker>& 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
Expand Down
2 changes: 2 additions & 0 deletions pj_plugins/include/pj_plugins/testing/toolbox_test_store.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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};
}
Expand Down
2 changes: 2 additions & 0 deletions pj_plugins/tests/toolbox_plugin_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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};
}
Expand Down
Loading