diff --git a/.claude/skills/plotjuggler-plugin/references/data-source.md b/.claude/skills/plotjuggler-plugin/references/data-source.md index b06b7db7..6e27610e 100644 --- a/.claude/skills/plotjuggler-plugin/references/data-source.md +++ b/.claude/skills/plotjuggler-plugin/references/data-source.md @@ -141,8 +141,7 @@ if (!r) return PJ::unexpected(r.error()); // a failed push IS an e The cache owns the `ensureParserBinding` + `pushMessage` pair per topic and anchors the payload bytes so the host's fetch callable is idempotent and by-value (the host may invoke it zero, one, or many times, from consumer threads, and releases it once -even when the push fails). The config-envelope that ties source⇆parser is managed -by the host; you never see the parser directly. Two delegated-ingest traps the +even when the push fails). You never see the parser directly. Two delegated-ingest traps the helpers already encode — know them anyway: - **Forward `_parser_config`.** The host injects the parser's saved options into diff --git a/.claude/skills/plotjuggler-plugin/references/message-parser.md b/.claude/skills/plotjuggler-plugin/references/message-parser.md index c15f7cae..04f53402 100644 --- a/.claude/skills/plotjuggler-plugin/references/message-parser.md +++ b/.claude/skills/plotjuggler-plugin/references/message-parser.md @@ -98,15 +98,14 @@ Key pieces: ## Route claims and functional extensions Recompile a handler-registering parser against SDK 0.22 and the base class -automatically exposes all three host-facing tables: +automatically exposes both host-facing tables: - `pj.parser_route_claims.v1` reports exact claims from the handler table. It never reports a wildcard. The host synthesizes one universal wildcard scalar claim per manifest encoding. -- `pj.parser_functional.v1` carries complete canonical-wire objects and scalar - records through synchronous caller-owned sinks. -- `pj.parser_functional.v2` keeps the scalar route unchanged and adds the object - splice sink. The host asks for v2 first and falls back to v1. +- `pj.parser_functional.v2` carries scalar records and canonical-wire objects + (complete, or with one spliced bulk field) through synchronous caller-owned + sinks. Scalar and object routes resolve independently. Another provider may therefore own the object route for a type that this parser still flattens to scalars. There diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d66d543..4c1f7209 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -94,6 +94,29 @@ SceneEntities and PlotMarkers: - `PlotMarkers`: the decoder drops a marker whose `kind` it does not know instead of drawing it as a time region; wire numbers unchanged. +Removed: + +- **Breaking (source):** the `pj.colormap.v1` service: `PJ_colormap_registry_vtable_t`, + `PJ_colormap_registry_t`, `sdk::ColorMapRegistryView`, `sdk::ColorMapRegistryService` and + `ToolboxPluginBase::colorMapRegistry()` / `colorMapRegistryBound()`. No replacement; no host + served it. The id `pj.colormap.v1` is retired and never reused (a future service would be + `pj.colormap.v2`). +- **Breaking (source):** `PJ::ConfigEnvelope` (`pj_plugins/host/config_envelope.hpp`). No + replacement; the host never used it. +- **Breaking (source):** the `descriptor_import_support` compatibility surface: the + `plotjuggler_sdk::descriptor_import_support` CMake target and Conan component, and the forwarding + headers under `pj_base/sdk/descriptor_import/` with their `PJ::sdk::descriptor_import` namespace + alias. Use `plotjuggler_sdk::source`, `pj_base/sdk/source/` and `PJ::sdk::source`. + `pj_base/sdk/descriptor_import.hpp` and the `pj.descriptor_import.v1` protocol are unchanged. +- **Breaking (source):** the `pj.parser_functional.v1` extension: `PJ_PARSER_FUNCTIONAL_EXTENSION_V1`, + `PJ_parser_functional_v1_t`, `PJ_parser_object_sink_v1_t`, `PJ_parser_parse_object_fn_t` and + their `*_V1_MIN_SIZE` macros. `MessageParserPluginBase` no longer answers the v1 id and + `MessageParserHandle` negotiates only `pj.parser_functional.v2`, treating a v1-only parser like one + without a functional extension. Use v2. The id `pj.parser_functional.v1` is retired and never + reused. +- **Breaking (source):** `PJ::sdk::MediaMetadataBuilder` (`pj_base/sdk/media_metadata.hpp`). Use + `ObjectTopicMetadataBuilder` (`pj_base/sdk/object_topic_metadata.hpp`). + ## [0.40.0] Host contract: extended: pj.mcp_endpoint.v1, PJ_mcp_endpoint_host_vtable_t::acquire_endpoint, diff --git a/CLAUDE.md b/CLAUDE.md index bb5187eb..4e67b0f1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -37,8 +37,7 @@ ownership or storage formats change. Read only the relevant reference sections. `pj_source` (`plotjuggler_sdk::source`, `PJ::sdk::source`) and `pj_plugins` depend on `pj_base`; both use nlohmann/json. Provider headers live under -`pj_base/sdk/source/`. The pre-0.31 `descriptor_import_support` component and -include directory forward to `source` for one release; new code uses `source`. +`pj_base/sdk/source/`. DataSource/MessageParser/Toolbox C protocols live in `pj_base`; the Dialog protocol lives in `pj_plugins/dialog_protocol/` and the Widget protocol in diff --git a/CMakeLists.txt b/CMakeLists.txt index 313c0429..a7618441 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -150,7 +150,7 @@ endif() # Components: # base — vocabulary types (always available) # plugin_sdk — plugin-author surface: base + dialog SDK + parser SDK -# source — compiled provider helpers (descriptor_import_support is an alias) +# source — compiled provider helpers # plugin_host — host-side loaders (data_source, message_parser, toolbox, # dialog, catalogs) # --------------------------------------------------------------------------- diff --git a/cmake/plotjuggler_sdkConfig.cmake.in b/cmake/plotjuggler_sdkConfig.cmake.in index 705f170f..3098756d 100644 --- a/cmake/plotjuggler_sdkConfig.cmake.in +++ b/cmake/plotjuggler_sdkConfig.cmake.in @@ -25,9 +25,9 @@ foreach(_comp ${plotjuggler_sdk_FIND_COMPONENTS}) include("${CMAKE_CURRENT_LIST_DIR}/PjSdkTestFixtures.cmake") set(plotjuggler_sdk_plugin_sdk_FOUND TRUE) - elseif(_comp STREQUAL "source" OR _comp STREQUAL "descriptor_import_support") + elseif(_comp STREQUAL "source") find_dependency(nlohmann_json) - set(plotjuggler_sdk_${_comp}_FOUND TRUE) + set(plotjuggler_sdk_source_FOUND TRUE) elseif(_comp STREQUAL "plugin_host") find_dependency(nlohmann_json) diff --git a/conanfile.py b/conanfile.py index ca8afc49..486af30f 100644 --- a/conanfile.py +++ b/conanfile.py @@ -6,7 +6,6 @@ plugin_sdk — umbrella for plugin authors (base + dialog SDK + parser SDK) plugin_host — umbrella for host loaders (data_source/parser/toolbox/dialog) source — compiled source-provider support - descriptor_import_support — compatibility alias for source A consuming Conan recipe declares `plotjuggler_sdk/` and then: @@ -178,11 +177,6 @@ def package_info(self): source.includedirs = ["include"] source.requires = ["base", "nlohmann_json::nlohmann_json"] - support = self.cpp_info.components["descriptor_import_support"] - support.set_property("cmake_target_name", "plotjuggler_sdk::descriptor_import_support") - support.libs = [] - support.requires = ["source"] - # --- plugin_host (umbrella linking every host-side loader) --- if self.options.with_host: diff --git a/docs/BACKLOG.md b/docs/BACKLOG.md index 51a947cc..d3aa52c2 100644 --- a/docs/BACKLOG.md +++ b/docs/BACKLOG.md @@ -8,9 +8,7 @@ imply that consumer repositories have adopted it. ## 0.31 delivery status The 2026-09-05 promotion plan is implemented below, with the 2026-09-06 -maintainer decisions on query/rate ownership. `pj_source` replaces the compiled -`descriptor_import_support` component; the old target and includes forward for -one release. New consumers use `plotjuggler_sdk::source` and `PJ::sdk::source`. +maintainer decisions on query/rate ownership. New consumers use `plotjuggler_sdk::source` and `PJ::sdk::source`. | Original item | Status and reuse destination | |---|---| diff --git a/docs/builtin_type.md b/docs/builtin_type.md index 51353cfe..d8d95a6c 100644 --- a/docs/builtin_type.md +++ b/docs/builtin_type.md @@ -91,7 +91,7 @@ are small enough that the zero-copy anchor pattern is unnecessary. **Serialize only at an explicit boundary.** Large byte-backed types remain views over source-native payload bytes while they are inside one component and an ownership anchor is available. Every stable builtin also has a canonical -codec for storage/replay and for the `pj.parser_functional.v1`/v2 DSO boundary. +codec for storage/replay and for the `pj.parser_functional.v2` DSO boundary. That boundary deliberately serializes the value rather than sharing C++ class, allocator, RTTI, or destructor state with the host. Functional v2 may carry one frozen eligible bulk field as a splice into the input payload; the host still @@ -162,7 +162,7 @@ For example, an image topic uses produce this field without a free-form type string. `object_type` and `media_class` are not renderer-discovery aliases. -`MediaMetadataBuilder` remains available for supplemental fields such as media +`ObjectTopicMetadataBuilder::string()` adds supplemental fields such as media classification, encoding, and schema, and custom untyped object topics remain valid. diff --git a/docs/provider-guide.md b/docs/provider-guide.md index f159bed4..70bc2b9b 100644 --- a/docs/provider-guide.md +++ b/docs/provider-guide.md @@ -8,10 +8,7 @@ A descriptor-import provider turns a saved request into a finite dataset. Link Your plugin owns request semantics, transport, trust, credentials, parser selection and UI. This guide uses the existing C ABI and adds no protocol slots. -`plotjuggler_sdk::descriptor_import_support`, its Conan component, and headers -under `pj_base/sdk/descriptor_import/` forward to `source` for one release. -Existing source consumers still build. Use `pj_base/sdk/source/` for new code. -The consumer extension views remain in `pj_base/sdk/descriptor_import.hpp`. +The consumer extension views live in `pj_base/sdk/descriptor_import.hpp`. ## Reuse the provider machinery diff --git a/docs/sdk-utilities.md b/docs/sdk-utilities.md index 59cc28ab..7f6e5922 100644 --- a/docs/sdk-utilities.md +++ b/docs/sdk-utilities.md @@ -60,7 +60,7 @@ CMake helpers become available through `find_package(plotjuggler_sdk)`. | `PJ::SemVer::parse, PJ::SemVer::isValid, PJ::SemVer::operator<=>; PJ::sdkVersion` | Parse/compare SDK and manifest versions | `pj_base/sdk/semver.hpp; pj_base/sdk/version.hpp` | | `PJ::WidgetEvent` | Decode raw widget events; DialogPluginTyped already dispatches typed handlers | `pj_plugins/sdk/widget_event.hpp` | | `PJ::FilePickerOptions, FilePickerResult, TreeItem, TreeCell` | Canonical picker/tree payloads and wire-value spellings | `pj_plugins/sdk/file_picker_types.hpp; pj_plugins/sdk/tree_types.hpp` | -| `PJ::sdk::MediaMetadataBuilder, ObjectTopicMetadataBuilder` | Build media hints and canonical renderer metadata | `pj_base/sdk/media_metadata.hpp; pj_base/sdk/object_topic_metadata.hpp` | +| `PJ::sdk::ObjectTopicMetadataBuilder` | Build canonical renderer metadata and supplemental media hints | `pj_base/sdk/object_topic_metadata.hpp` | | `PJ::sdk::ObjectBytes` | Own object-read bytes through move-only RAII | `pj_base/sdk/object_bytes.hpp` | | `PJ::sdk::ArrowSchemaHolder, ArrowArrayHolder, ArrowStreamHolder` | Own Arrow out-params; successful writes transfer ownership, failed writes retain it | `pj_base/sdk/arrow.hpp` | | `PJ::sdk::parseEvaluationReport` | Parse a data-processor poll report (COMPLETED or FAILED) and derive each bundle's stamp | `pj_plugins/sdk/evaluation_report.hpp` | diff --git a/examples/sdk_consumer/CMakeLists.txt b/examples/sdk_consumer/CMakeLists.txt index d528adbe..dc5205a0 100644 --- a/examples/sdk_consumer/CMakeLists.txt +++ b/examples/sdk_consumer/CMakeLists.txt @@ -3,7 +3,7 @@ project(sdk_consumer LANGUAGES CXX) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) -find_package(plotjuggler_sdk REQUIRED COMPONENTS plugin_sdk source descriptor_import_support) +find_package(plotjuggler_sdk REQUIRED COMPONENTS plugin_sdk source) # A plugin is: add_library(SHARED) + link plugin_sdk + pj_configure_plugin(). # The helper (shipped with plugin_sdk) applies symbol isolation, the export @@ -33,12 +33,12 @@ pj_embed_file(dialog_controls_example VAR_NAME kUiContent ) -# Probe for the descriptor_import_support component: links the exported -# compiled target and touches one symbol per public header. -add_executable(descriptor_import_probe descriptor_import_probe.cpp) -target_link_libraries(descriptor_import_probe PRIVATE plotjuggler_sdk::descriptor_import_support) +# Probe for the source component: links the exported compiled target and +# touches one symbol per public header. +add_executable(source_probe source_probe.cpp) +target_link_libraries(source_probe PRIVATE plotjuggler_sdk::source) -# New component and packaged fixture consumption, alongside the deprecated probe. +# Packaged fixture consumption. foreach(fixture mock_data_source mock_file_source mock_toolbox missing_id_data_source) pj_add_sdk_test_fixture(${fixture} fixture_target) # Repeated calls must return the same target without redefining it. diff --git a/examples/sdk_consumer/descriptor_import_probe.cpp b/examples/sdk_consumer/source_probe.cpp similarity index 80% rename from examples/sdk_consumer/descriptor_import_probe.cpp rename to examples/sdk_consumer/source_probe.cpp index 3b9debea..c8d2ebb6 100644 --- a/examples/sdk_consumer/descriptor_import_probe.cpp +++ b/examples/sdk_consumer/source_probe.cpp @@ -1,19 +1,19 @@ // Copyright 2026 Davide Faconti // SPDX-License-Identifier: Apache-2.0 // -// Install-test probe for the descriptor_import_support component: exercises +// Install-test probe for the source component: exercises // one symbol from each public header through the exported CMake target. #include #include -#include -#include -#include -#include +#include +#include +#include +#include #include int main() { - namespace di = PJ::sdk::descriptor_import; + namespace di = PJ::sdk::source; di::OriginPolicy origin_policy; origin_policy.allowed_schemes = {"grpc", "grpc+tls"}; diff --git a/packaging/conan-center-index/recipes/plotjuggler_sdk/all/conanfile.py b/packaging/conan-center-index/recipes/plotjuggler_sdk/all/conanfile.py index a7c2bd76..e6e6d32b 100644 --- a/packaging/conan-center-index/recipes/plotjuggler_sdk/all/conanfile.py +++ b/packaging/conan-center-index/recipes/plotjuggler_sdk/all/conanfile.py @@ -60,8 +60,8 @@ def layout(self): cmake_layout(self, src_folder="src") def requirements(self): - # nlohmann_json is in PUBLIC installed headers (config_envelope.hpp, - # widget_data, plugin_catalog) and is find_dependency()'d by the + # nlohmann_json is in PUBLIC installed headers (streaming_source.hpp, + # widget_data.hpp) and is find_dependency()'d by the # installed CMake usage, so it must propagate to consumers. self.requires("nlohmann_json/3.12.0", transitive_headers=True) diff --git a/pj_base/CMakeLists.txt b/pj_base/CMakeLists.txt index 911f0810..03bfdeef 100644 --- a/pj_base/CMakeLists.txt +++ b/pj_base/CMakeLists.txt @@ -114,18 +114,13 @@ set_target_properties(pj_source PROPERTIES EXPORT_NAME source ) add_library(plotjuggler_sdk::source ALIAS pj_source) -# Deprecated compatibility target, retained for one release. -add_library(pj_descriptor_import_support INTERFACE) -target_link_libraries(pj_descriptor_import_support INTERFACE pj_source) -set_target_properties(pj_descriptor_import_support PROPERTIES EXPORT_NAME descriptor_import_support) -add_library(plotjuggler_sdk::descriptor_import_support ALIAS pj_descriptor_import_support) # --------------------------------------------------------------------------- # Install (guarded by PJ_INSTALL_SDK in root CMakeLists.txt) # --------------------------------------------------------------------------- if(PJ_INSTALL_SDK) - install(TARGETS pj_base pj_source pj_descriptor_import_support EXPORT plotjuggler_sdkTargets + install(TARGETS pj_base pj_source EXPORT plotjuggler_sdkTargets ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} ) @@ -174,7 +169,6 @@ if(PJ_BUILD_TESTS) tests/image_annotations_codec_test.cpp tests/image_annotations_decoder_test.cpp tests/field_table_test.cpp - tests/media_metadata_test.cpp tests/object_topic_metadata_test.cpp tests/push_message_test.cpp tests/attach_source_record_test.cpp diff --git a/pj_base/feature_floors.json b/pj_base/feature_floors.json index 73b27922..d5b327b3 100644 --- a/pj_base/feature_floors.json +++ b/pj_base/feature_floors.json @@ -61,8 +61,6 @@ "PJ_DESCRIPTOR_IMPORT_START_FLAG_NONE", "PJ_PARSER_ROUTE_FLAG_OBJECT_V1", "PJ_PARSER_ROUTE_FLAG_SCALAR_V1", - "PJ_colormap_registry_vtable_t::register_map", - "PJ_colormap_registry_vtable_t::unregister_map", "PJ_data_processors_host_vtable_t::create_data_processor", "PJ_data_processors_host_vtable_t::data_processor_config", "PJ_data_processors_host_vtable_t::list_data_processor_ids", @@ -151,7 +149,6 @@ "PJ_toolbox_runtime_host_vtable_t::report_message", "PJ_viewport_host_vtable_t::zoom_reset", "PJ_viewport_host_vtable_t::zoom_to_time_range", - "pj.colormap.v1", "pj.data_processors.v1", "pj.parser_object_write.v1", "pj.parser_runtime.v1", diff --git a/pj_base/include/pj_base/builtin_object_abi.h b/pj_base/include/pj_base/builtin_object_abi.h index c3c6c6b1..fa2c20c6 100644 --- a/pj_base/include/pj_base/builtin_object_abi.h +++ b/pj_base/include/pj_base/builtin_object_abi.h @@ -8,7 +8,7 @@ * carrying a PJ_builtin_object_type_t. * * Canonical-object and pure-functional scalar production use the additive - * `pj.parser_functional.v1` and v2 C extensions. MessageParserPluginBase keeps + * `pj.parser_functional.v2` C extension. MessageParserPluginBase keeps * the plugin-author-facing ObjectRecord/ScalarRecord API inside the plugin * DSO; its trampolines emit only POD scalar views, canonical wire bytes, or a * v2 splice using the frozen eligibility table below. The concrete host-owned diff --git a/pj_base/include/pj_base/message_parser_protocol.h b/pj_base/include/pj_base/message_parser_protocol.h index 769dca6f..3ff1b091 100644 --- a/pj_base/include/pj_base/message_parser_protocol.h +++ b/pj_base/include/pj_base/message_parser_protocol.h @@ -9,11 +9,11 @@ * per-record, with an optional append_arrow_stream tail slot for * parser-shaped formats that naturally decode batches. * - * Pure-functional production uses the additive `pj.parser_functional.v1` and - * v2 extensions declared in parser_functional_protocol.h. Scalar fields cross + * Pure-functional production uses the additive `pj.parser_functional.v2` + * extension declared in parser_functional_protocol.h. Scalar fields cross * as synchronous borrowed C values; canonical objects cross as a stable type * tag plus canonical wire bytes or a v2 splice. No C++ parser class or STL - * value crosses the new path. Parsers built before SDK 0.21 do not expose the + * value crosses the new path. Parsers built before SDK 0.22 do not expose the * extension and may use a deprecated host compatibility bridge until the next * SDK major version. Pure-C plugins may continue using parse() to write * scalars to writeHost. diff --git a/pj_base/include/pj_base/parser_functional_protocol.h b/pj_base/include/pj_base/parser_functional_protocol.h index 88849703..8e3a4750 100644 --- a/pj_base/include/pj_base/parser_functional_protocol.h +++ b/pj_base/include/pj_base/parser_functional_protocol.h @@ -2,10 +2,10 @@ * @file parser_functional_protocol.h * @brief Additive C ABI extension for pure-functional MessageParser results. * - * A handler-based MessageParser exposes the v1 and v2 tables from - * get_plugin_extension after at least one SchemaHandler is registered. They - * replace direct host calls on MessageParserPluginBase with synchronous, - * caller-owned C sinks; v2 adds an optional single-field object splice. No + * A handler-based MessageParser exposes the v2 table from + * get_plugin_extension after at least one SchemaHandler is registered. It + * replaces direct host calls on MessageParserPluginBase with synchronous, + * caller-owned C sinks, including an optional single-field object splice. No * C++ object, STL container, exception, or plugin allocation survives an * extension call. */ @@ -26,7 +26,9 @@ extern "C" { #endif -#define PJ_PARSER_FUNCTIONAL_EXTENSION_V1 "pj.parser_functional.v1" +/* Extension id "pj.parser_functional.v1" is RETIRED as of 1.0.0 and must + * never be reused: plugins built against SDK 0.21 through 1.0.0-dev.1 export it + * with the old table layout. Hosts negotiate only "pj.parser_functional.v2". */ /** Caller-owned synchronous sink for one scalar record. * @@ -61,19 +63,7 @@ typedef bool (*PJ_parser_accept_object_fn_t)( void* ctx, bool has_timestamp, int64_t timestamp_ns, uint16_t object_type, PJ_bytes_view_t canonical_wire, PJ_error_t* out_error) PJ_NOEXCEPT; -typedef struct PJ_parser_object_sink_v1_t { - /** Initialize to sizeof(PJ_parser_object_sink_v1_t). Future revisions may - * append fields; providers must accept every compatible prefix. - */ - uint32_t struct_size; - void* ctx; - PJ_parser_accept_object_fn_t accept_object; -} PJ_parser_object_sink_v1_t; - -#define PJ_PARSER_OBJECT_SINK_V1_MIN_SIZE \ - (offsetof(PJ_parser_object_sink_v1_t, accept_object) + sizeof(PJ_parser_accept_object_fn_t)) - -/** Pure-functional parser extension v1. +/** Scalar parsing entry point of the functional extension. * * Calls are [stream-thread] and synchronous. A provider validates the sink * table before invoking plugin code. On success it calls the supplied sink @@ -84,26 +74,6 @@ typedef bool (*PJ_parser_parse_scalars_fn_t)( void* plugin_ctx, int64_t timestamp_ns, PJ_bytes_view_t payload, const PJ_parser_scalar_sink_v1_t* sink, PJ_error_t* out_error) PJ_NOEXCEPT; -/** Object parsing takes ownership of exactly one `payload.anchor` reference at - * function entry and releases it exactly once on every success/failure path. - * A null ctx + null release is a borrowed call-duration-only payload. The - * provider may propagate an owning anchor inside plugin-side C++ values while - * parsing, but all such values are destroyed before this C call returns. - */ -typedef bool (*PJ_parser_parse_object_fn_t)( - void* plugin_ctx, int64_t timestamp_ns, PJ_payload_t payload, const PJ_parser_object_sink_v1_t* sink, - PJ_error_t* out_error) PJ_NOEXCEPT; - -typedef struct PJ_parser_functional_v1_t { - /** sizeof(PJ_parser_functional_v1_t) for this append-only table revision. */ - uint32_t struct_size; - PJ_parser_parse_scalars_fn_t parse_scalars; - PJ_parser_parse_object_fn_t parse_object; -} PJ_parser_functional_v1_t; - -#define PJ_PARSER_FUNCTIONAL_V1_MIN_SIZE \ - (offsetof(PJ_parser_functional_v1_t, parse_object) + sizeof(PJ_parser_parse_object_fn_t)) - #define PJ_PARSER_FUNCTIONAL_EXTENSION_V2 "pj.parser_functional.v2" #define PJ_PARSER_ERROR_KIND_DATA_ERROR "pj.parser.data_error" #define PJ_PARSER_ERROR_KIND_SINK_REJECTED "pj.parser.sink_rejected" @@ -137,10 +107,13 @@ typedef struct PJ_parser_object_sink_v2_t { /** Pure-functional parser extension v2. * - * Calls are [stream-thread] and synchronous. Scalar parsing is unchanged - * from v1. Object parsing consumes exactly one payload anchor reference using - * the v1 ownership contract and may deliver either a complete canonical wire - * object or one splice-eligible object. Failures set `extended_kind` to one + * Calls are [stream-thread] and synchronous. Object parsing takes ownership + * of exactly one `payload.anchor` reference at function entry and releases it + * exactly once on every success/failure path. A null ctx + null release is a + * borrowed call-duration-only payload. The provider may propagate an owning + * anchor inside plugin-side C++ values while parsing, but all such values are + * destroyed before this C call returns. It may deliver either a complete + * canonical wire object or one splice-eligible object. Failures set `extended_kind` to one * of the frozen PJ_PARSER_ERROR_KIND_* values and leave `extended` null. */ typedef struct PJ_parser_functional_v2_t { diff --git a/pj_base/include/pj_base/plugin_data_api.h b/pj_base/include/pj_base/plugin_data_api.h index 48be68dc..4c1caf7c 100644 --- a/pj_base/include/pj_base/plugin_data_api.h +++ b/pj_base/include/pj_base/plugin_data_api.h @@ -49,7 +49,7 @@ extern "C" { * pre-v5 parser DSOs because the C++ MessageParserPluginBase * pure-functional contract changed parseScalars()/parseObject() return * types to ScalarRecord/ObjectRecord. SDK 0.21 subsequently placed those - * results behind the additive `pj.parser_functional.v1` C extension; direct + * results behind an additive C extension, now `pj.parser_functional.v2`; direct * host calls on the C++ base remain only as a temporary old-plugin bridge. * * The C data-plane layout remains the v4 layout: @@ -941,30 +941,11 @@ typedef struct { const PJ_object_read_host_vtable_t* vtable; } PJ_object_read_host_t; -/** - * Colormap registry service ("pj.colormap.v1", protocol_version 1). - * - * Independent host-provided service for toolbox plugins that want to - * publish named colormap callbacks. +/* + * Service id "pj.colormap.v1" is RETIRED as of 1.0.0 and must never be reused: + * toolbox binaries built against earlier SDKs request it with the old layout. + * A future colormap service would be "pj.colormap.v2". */ -typedef struct PJ_colormap_registry_vtable_t { - uint32_t protocol_version; - uint32_t struct_size; - - /* [main-thread] Register a named colormap. eval_fn is invoked later from - * the main GUI thread when rendering. */ - bool (*register_map)( - void* ctx, PJ_string_view_t name, const char* (*eval_fn)(double value, void* user_ctx), void* user_ctx, - PJ_error_t* out_error) PJ_NOEXCEPT; - - /* [main-thread] Unregister a previously registered colormap. */ - bool (*unregister_map)(void* ctx, PJ_string_view_t name, PJ_error_t* out_error) PJ_NOEXCEPT; -} PJ_colormap_registry_vtable_t; - -typedef struct { - void* ctx; - const PJ_colormap_registry_vtable_t* vtable; -} PJ_colormap_registry_t; /** * Data-processor host service ("pj.data_processors.v1", protocol_version 1). diff --git a/pj_base/include/pj_base/sdk/data_source_plugin_base.hpp b/pj_base/include/pj_base/sdk/data_source_plugin_base.hpp index 2da80418..181f1bc7 100644 --- a/pj_base/include/pj_base/sdk/data_source_plugin_base.hpp +++ b/pj_base/include/pj_base/sdk/data_source_plugin_base.hpp @@ -10,7 +10,7 @@ * - Optional: `bind()`, `pause()`, `resume()`, `poll()`, `saveConfig()`, * `loadConfig()`, `getDialog()`. * - Default `bind()` acquires the write host and runtime host from the - * service registry. Override to acquire extra services (colormap, etc.) + * service registry. Override to acquire extra services * or to relax the mandatory set. * - Use `writeHost()` and `runtimeHost()` inside `start()`/`poll()` to * interact with the host. Both return view classes that null-check and @@ -65,7 +65,7 @@ class DataSourcePluginBase { /// /// Plugins that don't write to ObjectStore simply leave `objectWriteHost()` /// unused; hosts without an ObjectStore bound simply don't register it. - /// Override to request additional services (e.g. colormap), or to relax + /// Override to request additional services, or to relax /// the default requirement. virtual Status bind(sdk::ServiceRegistry services); diff --git a/pj_base/include/pj_base/sdk/descriptor_import/origin.hpp b/pj_base/include/pj_base/sdk/descriptor_import/origin.hpp deleted file mode 100644 index 8664f262..00000000 --- a/pj_base/include/pj_base/sdk/descriptor_import/origin.hpp +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright 2026 Davide Faconti -// SPDX-License-Identifier: Apache-2.0 - -#pragma once - -// Deprecated: use pj_base/sdk/source/ and PJ::sdk::source; retained for one release. -#include "pj_base/sdk/source/origin.hpp" - -namespace PJ::sdk { -namespace descriptor_import = source; -} diff --git a/pj_base/include/pj_base/sdk/descriptor_import/provider_job.hpp b/pj_base/include/pj_base/sdk/descriptor_import/provider_job.hpp deleted file mode 100644 index c9a66897..00000000 --- a/pj_base/include/pj_base/sdk/descriptor_import/provider_job.hpp +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright 2026 Davide Faconti -// SPDX-License-Identifier: Apache-2.0 - -#pragma once - -// Deprecated: use pj_base/sdk/source/ and PJ::sdk::source; retained for one release. -#include "pj_base/sdk/source/provider_job.hpp" - -namespace PJ::sdk { -namespace descriptor_import = source; -} diff --git a/pj_base/include/pj_base/sdk/descriptor_import/request_cache.hpp b/pj_base/include/pj_base/sdk/descriptor_import/request_cache.hpp deleted file mode 100644 index 3afebd90..00000000 --- a/pj_base/include/pj_base/sdk/descriptor_import/request_cache.hpp +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright 2026 Davide Faconti -// SPDX-License-Identifier: Apache-2.0 - -#pragma once - -// Deprecated: use pj_base/sdk/source/ and PJ::sdk::source; retained for one release. -#include "pj_base/sdk/source/request_cache.hpp" - -namespace PJ::sdk { -namespace descriptor_import = source; -} diff --git a/pj_base/include/pj_base/sdk/descriptor_import/source_descriptor.hpp b/pj_base/include/pj_base/sdk/descriptor_import/source_descriptor.hpp deleted file mode 100644 index b8cf5cd0..00000000 --- a/pj_base/include/pj_base/sdk/descriptor_import/source_descriptor.hpp +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright 2026 Davide Faconti -// SPDX-License-Identifier: Apache-2.0 - -#pragma once - -// Deprecated: use pj_base/sdk/source/ and PJ::sdk::source; retained for one release. -#include "pj_base/sdk/source/source_descriptor.hpp" - -namespace PJ::sdk { -namespace descriptor_import = source; -} diff --git a/pj_base/include/pj_base/sdk/media_metadata.hpp b/pj_base/include/pj_base/sdk/media_metadata.hpp deleted file mode 100644 index a3dd6a69..00000000 --- a/pj_base/include/pj_base/sdk/media_metadata.hpp +++ /dev/null @@ -1,121 +0,0 @@ -#pragma once -// Copyright 2026 Davide Faconti -// SPDX-License-Identifier: Apache-2.0 - -#include -#include - -#include "pj_base/sdk/detail/json.hpp" - -namespace PJ::sdk { - -/// Builder for supplemental media fields in the `metadata_json` string -/// attached to an ObjectStore topic at registration time. `media_class`, -/// `encoding`, and `schema` describe media or decoder details; they do not -/// select a canonical built-in renderer. In particular, `media_class` is not -/// an alias for the canonical `builtin_object_type` discovery key. -/// -/// Example: -/// auto meta = MediaMetadataBuilder() -/// .mediaClass("image") -/// .encoding("jpeg") -/// .schema("sensor_msgs/CompressedImage") -/// .build(); -/// host.registerTopic(name, meta); -/// -/// For a renderable built-in object, use `ObjectTopicMetadataBuilder`, call -/// `builtinObjectType()`, and compose supplemental fields with its `string()` -/// method. Custom, untyped object topics may continue to use this builder. -/// Custom keys are available through `extra()` for format-specific metadata. -class MediaMetadataBuilder { - public: - /// Set a supplemental media classification. This does not select a built-in - /// renderer; use `ObjectTopicMetadataBuilder::builtinObjectType()` for that. - MediaMetadataBuilder& mediaClass(std::string_view v) { - media_class_ = v; - return *this; - } - - MediaMetadataBuilder& encoding(std::string_view v) { - encoding_ = v; - return *this; - } - - MediaMetadataBuilder& schema(std::string_view v) { - schema_ = v; - return *this; - } - - /// Append a raw JSON key/value pair. `value_json` must itself be valid - /// JSON (a quoted string, number, bool, object, or array). For a plain - /// string value prefer `extraString()`. - MediaMetadataBuilder& extra(std::string_view key, std::string_view value_json) { - appendExtra(key, value_json, /*quoted=*/false); - return *this; - } - - /// Append a key whose value is a plain string — the builder quotes and - /// escapes it. - MediaMetadataBuilder& extraString(std::string_view key, std::string_view value) { - appendExtra(key, value, /*quoted=*/true); - return *this; - } - - [[nodiscard]] std::string build() const { - std::string out; - out.reserve(64 + media_class_.size() + encoding_.size() + schema_.size() + extras_.size()); - out.push_back('{'); - bool first = true; - auto kv_string = [&](std::string_view key, std::string_view value) { - if (value.empty()) { - return; - } - if (!first) { - out.push_back(','); - } - first = false; - out.push_back('"'); - out.append(key); - out.append("\":\""); - detail::appendJsonEscaped(out, value); - out.push_back('"'); - }; - kv_string("media_class", media_class_); - kv_string("encoding", encoding_); - kv_string("schema", schema_); - if (!extras_.empty()) { - if (!first) { - out.push_back(','); - } - // extras_ is pre-formatted as "key1":value1,"key2":value2 ... with - // embedded separators; append as-is. - out.append(extras_); - } - out.push_back('}'); - return out; - } - - private: - std::string media_class_; - std::string encoding_; - std::string schema_; - std::string extras_; // pre-formatted inner fragments separated by ','. - - void appendExtra(std::string_view key, std::string_view value, bool quoted) { - if (!extras_.empty()) { - extras_.push_back(','); - } - extras_.push_back('"'); - extras_.append(key); - extras_.append("\":"); - if (quoted) { - extras_.push_back('"'); - detail::appendJsonEscaped(extras_, value); - extras_.push_back('"'); - } else { - extras_.append(value); - } - } -}; - -} // namespace PJ::sdk diff --git a/pj_base/include/pj_base/sdk/plugin_data_api.hpp b/pj_base/include/pj_base/sdk/plugin_data_api.hpp index 14d495d1..e8dd5135 100644 --- a/pj_base/include/pj_base/sdk/plugin_data_api.hpp +++ b/pj_base/include/pj_base/sdk/plugin_data_api.hpp @@ -1628,54 +1628,6 @@ class ToolboxHostView { } }; -// --------------------------------------------------------------------------- -// ColorMapRegistryView — typed C++ view over PJ_colormap_registry_t -// --------------------------------------------------------------------------- - -/// Signature of a color evaluation callback — mirrors the C ABI. -using ColorMapEvalFn = const char* (*)(double value, void* user_ctx); - -/// C++ wrapper around PJ_colormap_registry_t for plugins that publish -/// colormaps. Constructed from the fat pointer delivered via -/// `bind_colormap_registry`. Empty-constructible; `valid()` tells whether -/// the host exposed a registry. -class ColorMapRegistryView { - public: - ColorMapRegistryView() = default; - explicit ColorMapRegistryView(PJ_colormap_registry_t registry) : registry_(registry) {} - - [[nodiscard]] bool valid() const noexcept { - return registry_.vtable != nullptr && registry_.ctx != nullptr; - } - - /// Register (or replace) a named colormap. The new entry becomes active. - [[nodiscard]] Status registerMap(std::string_view name, ColorMapEvalFn eval_fn, void* user_ctx) const { - if (!valid() || registry_.vtable->register_map == nullptr) { - return unexpected("colormap registry is not bound"); - } - PJ_error_t err{}; - if (!registry_.vtable->register_map(registry_.ctx, toAbiString(name), eval_fn, user_ctx, &err)) { - return unexpected(errorToString(err)); - } - return okStatus(); - } - - /// Unregister a colormap by name. Clears the active selection if it matched. - [[nodiscard]] Status unregisterMap(std::string_view name) const { - if (!valid() || registry_.vtable->unregister_map == nullptr) { - return unexpected("colormap registry is not bound"); - } - PJ_error_t err{}; - if (!registry_.vtable->unregister_map(registry_.ctx, toAbiString(name), &err)) { - return unexpected(errorToString(err)); - } - return okStatus(); - } - - private: - PJ_colormap_registry_t registry_{}; -}; - // --------------------------------------------------------------------------- // DataProcessorsHostView — typed C++ view over PJ_data_processors_host_t // --------------------------------------------------------------------------- diff --git a/pj_base/include/pj_base/sdk/service_traits.hpp b/pj_base/include/pj_base/sdk/service_traits.hpp index 2c7ab093..23599257 100644 --- a/pj_base/include/pj_base/sdk/service_traits.hpp +++ b/pj_base/include/pj_base/sdk/service_traits.hpp @@ -163,15 +163,6 @@ struct ToolboxHostService { static_assert(detail::isValidServiceName(kName), "kName must match the pj naming rule"); }; -struct ColorMapRegistryService { - static constexpr const char* kName = "pj.colormap.v1"; - static constexpr uint32_t kMinVersion = 1; - using Raw = PJ_colormap_registry_t; - using Vtable = PJ_colormap_registry_vtable_t; - using View = ColorMapRegistryView; - static_assert(detail::isValidServiceName(kName), "kName must match the pj naming rule"); -}; - /// Optional service for plugins that submit WHOLE-SERIES data processors to the host /// by data (kind + language + script + input/output names + params JSON + flags). /// Nothing executable crosses the boundary; the host compiles, runs, and routes the 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 72620a07..acf373c4 100644 --- a/pj_base/include/pj_base/sdk/toolbox_plugin_base.hpp +++ b/pj_base/include/pj_base/sdk/toolbox_plugin_base.hpp @@ -138,7 +138,6 @@ class ToolboxPluginBase { /// Default implementation pulls: /// - "pj.toolbox_write.v1" → ToolboxHost (mandatory) /// - "pj.toolbox_runtime.v1" → RuntimeHost (mandatory) - /// - "pj.colormap.v1" → ColorMap (optional) /// - "pj.toolbox_object_read.v1" → ObjectReadHost (optional) /// /// Override to acquire additional services or relax defaults. @@ -206,10 +205,6 @@ class ToolboxPluginBase { return runtime_host_view_; } - [[nodiscard]] const sdk::ColorMapRegistryView& colorMapRegistry() const { - return colormap_view_; - } - /// Optional — returns nullptr when the host does not register /// `pj.toolbox_object_read.v1`. Transformer-style toolboxes check this /// before touching ObjectStore; scalar-only toolboxes never call it. @@ -223,15 +218,11 @@ class ToolboxPluginBase { [[nodiscard]] bool runtimeHostBound() const { return runtime_host_view_.valid(); } - [[nodiscard]] bool colorMapRegistryBound() const { - return colormap_view_.valid(); - } private: sdk::ServiceRegistry service_registry_{}; sdk::ToolboxHostView toolbox_host_view_{PJ_toolbox_host_t{}}; ToolboxRuntimeHostView runtime_host_view_{}; - sdk::ColorMapRegistryView colormap_view_{}; sdk::ToolboxObjectReadHostView object_read_host_view_{}; std::string config_buf_; diff --git a/pj_base/include/pj_base/toolbox_protocol.h b/pj_base/include/pj_base/toolbox_protocol.h index cedb1dfa..dff1a93b 100644 --- a/pj_base/include/pj_base/toolbox_protocol.h +++ b/pj_base/include/pj_base/toolbox_protocol.h @@ -169,7 +169,7 @@ typedef struct PJ_toolbox_vtable_t { /** * [main-thread] Bind host services. The host registers at least * "pj.toolbox_write.v1" and "pj.toolbox_runtime.v1"; optional services - * such as "pj.colormap.v1" may also be present. + * may also be present. */ bool (*bind)(void* ctx, PJ_service_registry_t registry, PJ_error_t* out_error) PJ_NOEXCEPT; diff --git a/pj_base/src/toolbox_plugin_base.cpp b/pj_base/src/toolbox_plugin_base.cpp index fe2f1d05..0adcb94a 100644 --- a/pj_base/src/toolbox_plugin_base.cpp +++ b/pj_base/src/toolbox_plugin_base.cpp @@ -82,11 +82,6 @@ Status ToolboxPluginBase::bind(sdk::ServiceRegistry services) { } runtime_host_view_ = *runtime; - // Colormap is optional — acquire opportunistically. - if (auto cm = services.get()) { - colormap_view_ = *cm; - } - // Object read is optional — transformer-style toolboxes resolve it. if (auto obj = services.get()) { object_read_host_view_ = *obj; diff --git a/pj_base/tests/abi_layout_sentinels_test.cpp b/pj_base/tests/abi_layout_sentinels_test.cpp index 1f784580..b62df964 100644 --- a/pj_base/tests/abi_layout_sentinels_test.cpp +++ b/pj_base/tests/abi_layout_sentinels_test.cpp @@ -166,7 +166,7 @@ static_assert(offsetof(PJ_builtin_object_splice_field_v1_t, reserved) == 2, "bui static_assert( offsetof(PJ_builtin_object_splice_field_v1_t, field_number) == 4, "builtin splice field_number offset pinned"); -// Parser functional extension v1. These caller-owned sink tables are the +// Parser functional extension. These caller-owned sink tables are the // only values crossing the new parser result boundary; freeze their first // published layout independently of the C++ convenience wrappers. #if INTPTR_MAX == INT64_MAX @@ -174,14 +174,6 @@ static_assert(offsetof(PJ_parser_scalar_sink_v1_t, struct_size) == 0, "scalar si static_assert(offsetof(PJ_parser_scalar_sink_v1_t, ctx) == 8, "scalar sink context pinned"); static_assert(offsetof(PJ_parser_scalar_sink_v1_t, accept_record) == 16, "scalar sink callback pinned"); static_assert(sizeof(PJ_parser_scalar_sink_v1_t) == 24, "scalar sink size pinned"); -static_assert(offsetof(PJ_parser_object_sink_v1_t, struct_size) == 0, "object sink prefix pinned"); -static_assert(offsetof(PJ_parser_object_sink_v1_t, ctx) == 8, "object sink context pinned"); -static_assert(offsetof(PJ_parser_object_sink_v1_t, accept_object) == 16, "object sink callback pinned"); -static_assert(sizeof(PJ_parser_object_sink_v1_t) == 24, "object sink size pinned"); -static_assert(offsetof(PJ_parser_functional_v1_t, struct_size) == 0, "functional extension prefix pinned"); -static_assert(offsetof(PJ_parser_functional_v1_t, parse_scalars) == 8, "functional scalar slot pinned"); -static_assert(offsetof(PJ_parser_functional_v1_t, parse_object) == 16, "functional object slot pinned"); -static_assert(sizeof(PJ_parser_functional_v1_t) == 24, "functional extension size pinned"); static_assert(offsetof(PJ_parser_object_sink_v2_t, struct_size) == 0, "v2 object sink prefix pinned"); static_assert(offsetof(PJ_parser_object_sink_v2_t, ctx) == 8, "v2 object sink context pinned"); diff --git a/pj_base/tests/descriptor_import_origin_test.cpp b/pj_base/tests/descriptor_import_origin_test.cpp index 6a0c91f9..31f2303a 100644 --- a/pj_base/tests/descriptor_import_origin_test.cpp +++ b/pj_base/tests/descriptor_import_origin_test.cpp @@ -8,17 +8,17 @@ #include -#include "pj_base/sdk/descriptor_import/origin.hpp" +#include "pj_base/sdk/source/origin.hpp" namespace { -using PJ::sdk::descriptor_import::Origin; -using PJ::sdk::descriptor_import::originAllowed; -using PJ::sdk::descriptor_import::originKey; -using PJ::sdk::descriptor_import::OriginPolicy; -using PJ::sdk::descriptor_import::parseOrigin; -using PJ::sdk::descriptor_import::parseOriginList; -using PJ::sdk::descriptor_import::sameOrigin; +using PJ::sdk::source::Origin; +using PJ::sdk::source::originAllowed; +using PJ::sdk::source::originKey; +using PJ::sdk::source::OriginPolicy; +using PJ::sdk::source::parseOrigin; +using PJ::sdk::source::parseOriginList; +using PJ::sdk::source::sameOrigin; OriginPolicy grpcPolicy() { OriginPolicy policy; diff --git a/pj_base/tests/descriptor_import_provider_job_test.cpp b/pj_base/tests/descriptor_import_provider_job_test.cpp index 256e4c41..44923587 100644 --- a/pj_base/tests/descriptor_import_provider_job_test.cpp +++ b/pj_base/tests/descriptor_import_provider_job_test.cpp @@ -16,7 +16,7 @@ #include #include "pj_base/sdk/descriptor_import.hpp" -#include "pj_base/sdk/descriptor_import/provider_job.hpp" +#include "pj_base/sdk/source/provider_job.hpp" #include "pj_base/sdk/testing/provider_job_probe.hpp" namespace { @@ -24,11 +24,11 @@ namespace { using namespace std::chrono_literals; using PJ::readDescriptorImportStartRequest; using PJ::writeDescriptorQueryResult; -using PJ::sdk::descriptor_import::ImportOutcome; -using PJ::sdk::descriptor_import::JobControl; -using PJ::sdk::descriptor_import::ProviderJob; -using PJ::sdk::descriptor_import::SettlementLatch; -using PJ::sdk::descriptor_import::testing::startWithGateProbe; +using PJ::sdk::source::ImportOutcome; +using PJ::sdk::source::JobControl; +using PJ::sdk::source::ProviderJob; +using PJ::sdk::source::SettlementLatch; +using PJ::sdk::source::testing::startWithGateProbe; struct Recorder { std::atomic datasets{0}; diff --git a/pj_base/tests/descriptor_import_request_cache_test.cpp b/pj_base/tests/descriptor_import_request_cache_test.cpp index f7d8bff8..25cc65e8 100644 --- a/pj_base/tests/descriptor_import_request_cache_test.cpp +++ b/pj_base/tests/descriptor_import_request_cache_test.cpp @@ -16,19 +16,19 @@ #include #include -#include "pj_base/sdk/descriptor_import/request_cache.hpp" +#include "pj_base/sdk/source/request_cache.hpp" #include "pj_base/sdk/testing/request_cache_probe.hpp" namespace { namespace fs = std::filesystem; -using PJ::sdk::descriptor_import::ArtifactValidator; -using PJ::sdk::descriptor_import::CacheSpec; -using PJ::sdk::descriptor_import::CleanupPolicy; -using PJ::sdk::descriptor_import::IdentityScheme; -using PJ::sdk::descriptor_import::ReadLease; -using PJ::sdk::descriptor_import::RequestArtifactCache; -using PJ::sdk::descriptor_import::testing::cleanupWithProbe; +using PJ::sdk::source::ArtifactValidator; +using PJ::sdk::source::CacheSpec; +using PJ::sdk::source::CleanupPolicy; +using PJ::sdk::source::IdentityScheme; +using PJ::sdk::source::ReadLease; +using PJ::sdk::source::RequestArtifactCache; +using PJ::sdk::source::testing::cleanupWithProbe; const std::string kPrefix = "test:v1:sha256/128:"; const std::string kSuffix = ".artifact"; diff --git a/pj_base/tests/descriptor_import_source_descriptor_test.cpp b/pj_base/tests/descriptor_import_source_descriptor_test.cpp index a9c9e6af..980f7b63 100644 --- a/pj_base/tests/descriptor_import_source_descriptor_test.cpp +++ b/pj_base/tests/descriptor_import_source_descriptor_test.cpp @@ -9,16 +9,16 @@ #include -#include "pj_base/sdk/descriptor_import/source_descriptor.hpp" +#include "pj_base/sdk/source/source_descriptor.hpp" namespace { -using PJ::sdk::descriptor_import::canonicalSourceDescriptorJson; -using PJ::sdk::descriptor_import::IdentityScheme; -using PJ::sdk::descriptor_import::parseSourceDescriptor; -using PJ::sdk::descriptor_import::sha256Hex; -using PJ::sdk::descriptor_import::sourceDescriptorIdentity; -using PJ::sdk::descriptor_import::SourceDescriptorPolicy; +using PJ::sdk::source::canonicalSourceDescriptorJson; +using PJ::sdk::source::IdentityScheme; +using PJ::sdk::source::parseSourceDescriptor; +using PJ::sdk::source::sha256Hex; +using PJ::sdk::source::sourceDescriptorIdentity; +using PJ::sdk::source::SourceDescriptorPolicy; SourceDescriptorPolicy mosaicoPolicy() { SourceDescriptorPolicy policy; @@ -131,7 +131,7 @@ TEST(SourceDescriptor, DeniedKeysRejectAtAnyDepth) { policy.denied_keys = {"password"}; const auto denied = parseSourceDescriptor(R"({"topics":[{"nested":{"password":"x"}}]})", policy); ASSERT_FALSE(denied); - EXPECT_EQ(denied.error(), std::string(PJ::sdk::descriptor_import::kDeniedKeyViolationPrefix) + "password"); + EXPECT_EQ(denied.error(), std::string(PJ::sdk::source::kDeniedKeyViolationPrefix) + "password"); // Exact, case-sensitive key match only; values are never inspected. EXPECT_TRUE(parseSourceDescriptor(R"({"topics":[{"Password":"x","value":"password"}]})", policy)); } diff --git a/pj_base/tests/media_metadata_test.cpp b/pj_base/tests/media_metadata_test.cpp deleted file mode 100644 index bae04ce5..00000000 --- a/pj_base/tests/media_metadata_test.cpp +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright 2026 Davide Faconti -// SPDX-License-Identifier: Apache-2.0 - -#include "pj_base/sdk/media_metadata.hpp" - -#include - -#include - -namespace PJ::sdk { -namespace { - -TEST(MediaMetadataBuilderTest, EmptyBuilderEmitsEmptyObject) { - EXPECT_EQ(MediaMetadataBuilder().build(), "{}"); -} - -TEST(MediaMetadataBuilderTest, SingleKeyRoundtrip) { - EXPECT_EQ(MediaMetadataBuilder().mediaClass("image").build(), R"({"media_class":"image"})"); - EXPECT_EQ(MediaMetadataBuilder().encoding("jpeg").build(), R"({"encoding":"jpeg"})"); - EXPECT_EQ( - MediaMetadataBuilder().schema("sensor_msgs/CompressedImage").build(), - R"({"schema":"sensor_msgs/CompressedImage"})"); -} - -TEST(MediaMetadataBuilderTest, AllThreeKeysInCanonicalOrder) { - const auto json = MediaMetadataBuilder().mediaClass("video").encoding("h264").schema("PJ.VideoFrame").build(); - EXPECT_EQ(json, R"({"media_class":"video","encoding":"h264","schema":"PJ.VideoFrame"})"); -} - -TEST(MediaMetadataBuilderTest, EmptyKeysAreOmitted) { - const auto json = MediaMetadataBuilder().mediaClass("image").schema("").build(); - EXPECT_EQ(json, R"({"media_class":"image"})"); -} - -TEST(MediaMetadataBuilderTest, ExtraStringIsQuoted) { - const auto json = MediaMetadataBuilder().mediaClass("image").extraString("source", "camera_0").build(); - EXPECT_EQ(json, R"({"media_class":"image","source":"camera_0"})"); -} - -TEST(MediaMetadataBuilderTest, ExtraRawJsonIsPassedThrough) { - const auto json = MediaMetadataBuilder().mediaClass("video").extra("width", "1920").extra("height", "1080").build(); - EXPECT_EQ(json, R"({"media_class":"video","width":1920,"height":1080})"); -} - -TEST(MediaMetadataBuilderTest, EscapesQuotesAndBackslashes) { - // Use ordinary escaped string literals (not raw strings) because the - // MSVC preprocessor on the CI runner mishandles raw-string tokenization - // when the body contains `"` and `\` — it drops out of raw-string mode - // and reinterprets the tail as a user-defined literal suffix. Escaped - // literals carry identical bytes and are accepted by every compiler. - const auto json = MediaMetadataBuilder().schema("weird\"name\\with").build(); - EXPECT_EQ(json, "{\"schema\":\"weird\\\"name\\\\with\"}"); -} - -TEST(MediaMetadataBuilderTest, EscapesControlChars) { - std::string schema; - schema.push_back('\n'); - schema.push_back('\t'); - schema.push_back('\x01'); - const auto json = MediaMetadataBuilder().schema(schema).build(); - // Short escapes for \n and \t; hex escape for other control chars. - std::string expected = "{\"schema\":\"\\n\\t\\u0001\"}"; - EXPECT_EQ(json, expected); -} - -TEST(MediaMetadataBuilderTest, MultipleExtrasChain) { - const auto json = - MediaMetadataBuilder().mediaClass("image").extraString("frame_id", "base_link").extra("fps", "30.0").build(); - EXPECT_EQ(json, R"({"media_class":"image","frame_id":"base_link","fps":30.0})"); -} - -} // namespace -} // namespace PJ::sdk diff --git a/pj_plugins/CLAUDE.md b/pj_plugins/CLAUDE.md index 2e80dd45..f8ff3f09 100644 --- a/pj_plugins/CLAUDE.md +++ b/pj_plugins/CLAUDE.md @@ -16,7 +16,7 @@ order, then read this file or [pj_base/CLAUDE.md](../pj_base/CLAUDE.md) as relev handles. `plugin_catalog` scans embedded manifests (`scanPluginDsos` / `inspectPluginDso`). `ParserClaimCatalog` and `ParserRouteResolver` handle parser claim admission and per-route resolution. Also contains - `ServiceRegistryBuilder` and `ConfigEnvelope`. + `ServiceRegistryBuilder`. The DSO catalog that resolves duplicates and composes loaded plugin families is **host policy**. It lives in the app (`pj_runtime`, `PluginRuntimeCatalog`). - `include/pj_plugins/sdk/` — SDK pieces that live here, not in `pj_base`: diff --git a/pj_plugins/docs/ARCHITECTURE.md b/pj_plugins/docs/ARCHITECTURE.md index 8b931e15..7cdff2c5 100644 --- a/pj_plugins/docs/ARCHITECTURE.md +++ b/pj_plugins/docs/ARCHITECTURE.md @@ -62,7 +62,7 @@ validate `min_sdk_required` against. - **ABI-APPENDABLE**: all `*_vtable_t` types, service-host vtables, `PJ_service_registry_vtable_t`, `PJ_dialog_host_info_t`, `PJ_widget_build_info_t`, the - `pj.parser_functional.v1`/v2 extension and sink tables, and + `pj.parser_functional.v2` extension and sink tables, and `pj.parser_route_claims.v1`. New fields or slots go at the tail; readers honor the caller-provided size, and vtable slots are read with `PJ_HAS_TAIL_SLOT`. @@ -137,13 +137,12 @@ Stop/completion ordering and delegated-ingest fixtures. The [toolbox walkthrough](toolbox-guide.md#descriptor-import-and-source-promotion-0200) covers the extension wiring. -Stable MessageParser-specific examples are `"pj.parser_functional.v1"` and -`"pj.parser_functional.v2"` (`PJ_parser_functional_v1_t` and -`PJ_parser_functional_v2_t` in `pj_base/parser_functional_protocol.h`) plus the +Stable MessageParser-specific examples are `"pj.parser_functional.v2"` +(`PJ_parser_functional_v2_t` in `pj_base/parser_functional_protocol.h`) plus the exact route classifier `"pj.parser_route_claims.v1"`. A handler-registering -`MessageParserPluginBase` exposes all three automatically. The host queries v2 -first and falls back to v1; v2 adds one eligible object-field splice while -leaving scalar parsing unchanged. Route classification reports exact handler +`MessageParserPluginBase` exposes both automatically. Functional v2 may carry +one eligible object-field splice. The id `"pj.parser_functional.v1"` is retired +and never negotiated. Route classification reports exact handler coverage only, and the host synthesizes the universal wildcard scalar claim. Hosts must query after `bind_schema` and must not cache an earlier absence because schema-generic plugins may register their handler while binding. A @@ -210,9 +209,9 @@ service registry, error out-params, and typed borrowed-dialog patterns): - **Service registry as the sole binding mechanism.** Plugin vtables expose a single `bind(ctx, registry, err)` slot. The host registers all services - (write hosts, runtime hosts, colormap, settings, etc.) under canonical + (write hosts, runtime hosts, settings, etc.) under canonical reverse-DNS-style names (e.g. `"pj.source_write.v1"`, - `"pj.runtime.v1"`, `"pj.parser_runtime.v1"`, `"pj.toolbox_runtime.v1"`, `"pj.colormap.v1"`, + `"pj.runtime.v1"`, `"pj.parser_runtime.v1"`, `"pj.toolbox_runtime.v1"`, `"pj.settings.v1"`, `"pj.data_processors.v1"`, `"pj.source_promotion.v1"`). Plugins acquire only the services they use. `"pj.source_promotion.v1"` (optional, bound per plugin instance) lets a descriptor-import provider hand @@ -352,7 +351,7 @@ pj_base/ include/pj_base/ data_source_protocol.h ← C ABI message_parser_protocol.h ← C ABI - parser_functional_protocol.h ← C ABI: pj.parser_functional.v1/v2 sinks + parser_functional_protocol.h ← C ABI: pj.parser_functional.v2 sinks parser_route_claims_protocol.h ← C ABI: exact parser route classification toolbox_protocol.h ← C ABI plugin_data_api.h ← shared data-plane ABI (write hosts) @@ -409,7 +408,6 @@ pj_plugins/ parser_claim_catalog.hpp ← parser claims, admission, plugin-claim synthesis parser_route_resolver.hpp ← ordered per-route selection + probe cache service_registry_builder.hpp ← service wiring into bind() - config_envelope.hpp ← versioned config wrapper include/pj_plugins/sdk/ message_parser_plugin_base.hpp ← C++ SDK (parser base lives here, NOT pj_base) object_ingest_policy.hpp ← ObjectIngestPolicyResolver @@ -651,20 +649,6 @@ A plugin can request the host to close the dialog with OK by setting `__request_accept` in `widget_data()`. The engine checks this flag after applying widget state and calls `dialog->accept()` if set. -## 8. Config Envelope - -`ConfigEnvelope` (`config_envelope.hpp`) wraps a DataSource's plugin-owned -config alongside host-owned parser binding state: - -```json -{"version": 1, "source_config": "...", "parser_binding": "..."} -``` - -- `pack(source_config, parser_binding)` → envelope JSON string. -- `unpack(envelope_json)` → `Expected` with both fields. -- The source plugin never sees `parser_binding` — the host manages it. -- Used for layout save/restore of delegated-ingest sources. - ## 9. Plugin Data Host Bridge The data-plane bridge lives in `pj_datastore` and connects plugin write @@ -764,7 +748,7 @@ out_array, err)`: | `data_source_library_test.cpp` | Library loading, vtable validation | | `file_source_integration_test.cpp` | FileSourceBase end-to-end | | `delegated_ingest_integration_test.cpp` | _Disabled (pending v3-port: uses removed `bindWriteHost`/`bindRuntimeHost`/`get_last_error`; CMake target commented out). Coverage currently provided by `data_source_library_test.cpp` + `message_parser_library_test.cpp`._ | -| `source_dialog_integration_test.cpp` | DataSource dialog + config envelope | +| `source_dialog_integration_test.cpp` | DataSource dialog | | `message_parser_library_test.cpp` | Parser library loading | | `toolbox_plugin_test.cpp` | Toolbox loading, host binding, read+write flow | | `dialog_handle_test.cpp` | Owned and borrowed handle lifecycle | @@ -813,8 +797,8 @@ that cascades `topic > source > type > default`: Parsers participate through `classifySchema`, `parseScalars`, and `parseObject`, backed by the per-schema `SchemaHandler` table. Those C++ methods and their `ScalarRecord` / `ObjectRecord` / `BuiltinObject` values stay -inside the plugin DSO. The base exposes both `pj.parser_functional.v1` and v2; -`MessageParserHandle` negotiates v2 first and falls back to v1: +inside the plugin DSO. The base exposes `pj.parser_functional.v2`, which +`MessageParserHandle` negotiates: - `parse_scalars` calls one caller-owned sink exactly once. Field/name/string views are borrowed only for that callback; the host copies anything it @@ -852,10 +836,10 @@ zero-length canonical payload is accepted as the valid proto3 default message when the separate type tag is known. Concrete builtins and codecs live under `pj_base/builtin/`; see `docs/builtin_type.md` for the catalog. -Pre-0.21 parsers expose no functional extension, and handler-based 0.21 parsers -may expose v1 without v2. The host keeps a clearly isolated, deprecated -direct-C++ bridge for binaries with neither extension. New plugins and new host -code use v2-first negotiation; SDK 1.0 can remove the bridge and the frozen +Parsers built before SDK 0.22 expose no usable functional extension: pre-0.21 +builds expose none, and 0.21 builds expose only the retired v1. The host keeps a +clearly isolated, deprecated direct-C++ bridge for those binaries. New plugins +and new host code use v2; SDK 1.0 can remove the bridge and the frozen `MessageParserPluginBase` layout constraint. ## Per-topic pause (demand-driven subscription) diff --git a/pj_plugins/docs/REQUIREMENTS.md b/pj_plugins/docs/REQUIREMENTS.md index dc0aeaf2..56530e4b 100644 --- a/pj_plugins/docs/REQUIREMENTS.md +++ b/pj_plugins/docs/REQUIREMENTS.md @@ -116,11 +116,11 @@ Independent decoder, typically driven by a DataSource via the host. JSON. - Lifecycle: create → bind → parse* → destroy. - A parser that registers `SchemaHandler` entries exposes - `pj.parser_functional.v1`, `pj.parser_functional.v2`, and + `pj.parser_functional.v2` and `pj.parser_route_claims.v1` after schema binding. Functional scalar and object results cross only through synchronous caller-owned C sinks; v2 may represent - one eligible object bulk field as a payload-relative splice. The host queries - v2 first, falls back to v1, and must never cast the opaque context to + one eligible object bulk field as a payload-relative splice. The host must + never cast the opaque context to `MessageParserPluginBase` for such a parser. - Route classification reports exact handler-table claims only. The host owns each encoding's universal wildcard scalar claim and resolves scalar and object @@ -135,10 +135,10 @@ Independent decoder, typically driven by a DataSource via the host. pays the explicit canonical serialization boundary. - Extension presence is truthful, not merely SDK-version-based: a rebuilt parser that still overrides only legacy `parse()` must not advertise the - functional extensions. Hosts query after `bind_schema` and do not cache an - earlier absence; an older handler parser may expose v1 without v2. + functional extension. Hosts query after `bind_schema` and do not cache an + earlier absence. - Until SDK 1.0, a host may use an isolated deprecated direct-C++ bridge for a - pre-0.21 parser whose functional extensions are absent. The bridge and its + pre-0.22 parser, which exposes no v2 extension. The bridge and its class-layout constraint are removed together at 1.0. ### Dialog @@ -306,14 +306,10 @@ to all Toolbox plugins. - The plugin decides what state it persists. - The host restores state deterministically from `loadConfig()` alone — hidden ambient sources like `QSettings` are not required. -- For delegated sources, the host wraps source and parser config in a - versioned `ConfigEnvelope`: - -```json -{"version": 1, "source_config": "...", "parser_binding": "..."} -``` - -The source never sees `parser_binding`; the host manages it. +- The host stores the JSON string returned by `saveConfig()` and passes it back to + `loadConfig()`. When the source dialog embeds a parser dialog, the host adds a + `_parser_config` key to that JSON, which `PJ::sdk::parserConfigOverride()` + (`pj_plugins/sdk/streaming_source.hpp`) reads. ## 8. External SDK Consumption diff --git a/pj_plugins/docs/data-source-guide.md b/pj_plugins/docs/data-source-guide.md index a447583d..db931a70 100644 --- a/pj_plugins/docs/data-source-guide.md +++ b/pj_plugins/docs/data-source-guide.md @@ -188,7 +188,7 @@ class CsvFileLoader : public PJ::FileSourceBase { PJ::Status loadConfig(std::string_view json) override { config_ = std::string(json); - // Extract "filepath" from the JSON config envelope. + // Extract "filepath" from the JSON config. // Use whatever JSON library your plugin links. // path_ = parse(json)["filepath"]; return PJ::okStatus(); @@ -640,9 +640,7 @@ evolution automatically. **Integrated parser dialog:** Add a `pj_parser_slot` placeholder to render the selected parser's configuration dialog inline. The host detects the placeholder. -Both dialogs share a window but persist config independently: -`ConfigEnvelope.source_config` for the source and `ConfigEnvelope.parser_binding` -for the parser. See `pj_plugins/docs/message-parser-guide.md` § "Dialog integration". +See `pj_plugins/docs/message-parser-guide.md` § "Dialog integration". ## Lifecycle Signals @@ -968,14 +966,9 @@ because the source owns the dialog object itself. `loadConfig()` restores it. Headless restart: `loadConfig(saved) → start()` — no dialog needed. -The host wraps the source config in a versioned envelope (`ConfigEnvelope`) -that also holds host-owned parser binding state: - -```json -{"version": 1, "source_config": "...", "parser_binding": "..."} -``` - -The source never sees `parser_binding` — the host manages it. +When the source dialog embeds a parser dialog, the host adds a +`_parser_config` key to that JSON, which `PJ::sdk::parserConfigOverride()` +(`pj_plugins/sdk/streaming_source.hpp`) reads. A complete example lives at `pj_plugins/examples/mock_source_with_dialog.cpp`. See `pj_plugins/docs/dialog-plugin-guide.md` for the dialog protocol itself. diff --git a/pj_plugins/docs/message-parser-guide.md b/pj_plugins/docs/message-parser-guide.md index f5a7f0d2..cd86e220 100644 --- a/pj_plugins/docs/message-parser-guide.md +++ b/pj_plugins/docs/message-parser-guide.md @@ -4,7 +4,7 @@ > write-host supports per-record writes and an optional Arrow stream > batch path for parser-shaped formats that naturally decode batches. > SchemaHandler-based parsers additionally expose -> `pj.parser_functional.v1`, `pj.parser_functional.v2`, and +> `pj.parser_functional.v2` and > `pj.parser_route_claims.v1`; no C++ result object crosses the plugin boundary. > For ABI evolution rules, error semantics, and noexcept discipline see > `ARCHITECTURE.md`. @@ -293,19 +293,17 @@ if (!status) { ## Optional Features -### Route claims and functional v1/v2 +### Route claims and functional v2 -Registering at least one `SchemaHandler` automatically exposes three extension +Registering at least one `SchemaHandler` automatically exposes two extension tables; parser authors do not implement them by hand: - `pj.parser_route_claims.v1` classifies exact handler-table coverage. It never reports wildcard claims. The host synthesizes the universal wildcard scalar claim from each manifest encoding. -- `pj.parser_functional.v1` delivers scalar records and complete canonical-wire - objects through synchronous caller-owned sinks. -- `pj.parser_functional.v2` keeps the scalar contract and adds a sink for one - splice-eligible object field. `MessageParserHandle` queries v2 first and falls - back to v1 when it is absent. +- `pj.parser_functional.v2` delivers scalar records and canonical-wire objects + through synchronous caller-owned sinks, including one splice-eligible object + field. The host resolves scalar and object routes independently. A parser may keep the scalar route for a type it flattens while another provider owns that type's @@ -430,25 +428,20 @@ placeholder in the source dialog. The host detects it and resolves the parser library for the selected encoding. It creates an owned parser dialog and renders it in the slot. -Both dialogs share a window but persist config independently: -`ConfigEnvelope.source_config` for the source and `ConfigEnvelope.parser_binding` -for the parser. - **Standalone:** The host shows the parser dialog as a modal from a settings panel or parser-selection UI. This is host-application logic, not protocol. #### Config flow for delegated sources -1. User configures the parser in the dialog → `parser_dialog.save_config()` → - host stores the JSON in `ConfigEnvelope.parser_binding`. -2. Source calls `runtimeHost().ensureParserBinding(request)` — the source may +The host stores the JSON string returned by `saveConfig()` and passes it back to +`loadConfig()`. When the source dialog embeds a parser dialog, the host adds a +`_parser_config` key to that JSON, which `PJ::sdk::parserConfigOverride()` +(`pj_plugins/sdk/streaming_source.hpp`) reads. + +1. Source calls `runtimeHost().ensureParserBinding(request)` — the source may leave `parser_config_json` empty in the request. -3. Host intercepts, injects the stored parser config into +2. Host intercepts, injects the stored parser config into `parser.load_config()` before calling `parse()`. -4. On layout save, the host persists the full `ConfigEnvelope`. -5. On headless restart, the host loads the envelope → - `source.loadConfig(source_config)` + restores parser bindings from - `parser_binding` → `source.start()`. #### Protobuf example sketch @@ -658,8 +651,8 @@ handler.parse_object = matching viewer. The types above are plugin-author C++ conveniences, not the binary boundary. -After `bindSchema`, `MessageParserPluginBase` advertises functional v1 and v2 -when at least one handler exists. Their trampolines: +After `bindSchema`, `MessageParserPluginBase` advertises functional v2 +when at least one handler exists. Its trampolines: 1. invokes `parseScalars` / `parseObject` inside the plugin DSO; 2. delivers scalar fields through one synchronous caller-owned C sink, or @@ -671,7 +664,7 @@ when at least one handler exists. Their trampolines: `MessageParserHandle::parseScalarsFunctional()` copies or consumes scalar views only during the callback. -`parseObjectFunctional()` negotiates v2 first and reconstructs a valid splice +`parseObjectFunctional()` uses the v2 extension and reconstructs a valid splice when present. It checks that the emitted type matches the binding's expected object type, then decodes synchronously into a host-owned `ObjectRecord`. That value remains safe after the parser instance and shared library are gone. @@ -688,11 +681,11 @@ This avoids exporting plugin allocators, destructors, RTTI, STL layout or type-erasure internals. Measure this path for image/point-cloud workloads. Do not bypass it with direct C++ calls. -Parsers built before SDK 0.21 expose no functional extension, and handler-based -0.21 parsers may expose v1 without v2. New host code uses v2-first/v1-fallback -negotiation and may use a deprecated, isolated direct-C++ bridge only when both -extensions are absent. It must not otherwise cast an opaque parser context; SDK -1.0 removes the bridge. +Parsers built before SDK 0.22 expose no usable functional extension: pre-0.21 +builds expose none, and 0.21 builds expose only the retired v1. New host code +negotiates v2 and may use a deprecated, isolated direct-C++ bridge only when v2 +is absent. It must not otherwise cast an opaque parser context; SDK 1.0 removes +the bridge. Builtin types and their canonical codecs live under `pj_base/builtin/`, one header per type. `serializeBuiltinObject()` / `deserializeBuiltinObject()` diff --git a/pj_plugins/docs/toolbox-guide.md b/pj_plugins/docs/toolbox-guide.md index 382185d1..eed21f3c 100644 --- a/pj_plugins/docs/toolbox-guide.md +++ b/pj_plugins/docs/toolbox-guide.md @@ -607,8 +607,6 @@ C++ consumers: `PJ::DescriptorImportProviderView`, `PJ::JoinableJob`, Start with [Existing SDK utilities](../../docs/sdk-utilities.md), then follow [the provider contract](../../docs/provider-guide.md). It covers the existing job/cache machinery, descriptor validation, completion, Stop and test fixtures. -The pre-0.31 `descriptor_import_support` component and include directory forward -to `source` for one release; new code uses `pj_base/sdk/source/`. ## Common Mistakes diff --git a/pj_plugins/include/pj_plugins/host/config_envelope.hpp b/pj_plugins/include/pj_plugins/host/config_envelope.hpp deleted file mode 100644 index b7d55937..00000000 --- a/pj_plugins/include/pj_plugins/host/config_envelope.hpp +++ /dev/null @@ -1,58 +0,0 @@ -#pragma once -// Copyright 2026 Davide Faconti -// SPDX-License-Identifier: Apache-2.0 - -/** - * @file config_envelope.hpp - * @brief Versioned wrapper pairing a DataSource's plugin-owned config with - * host-owned parser-binding state for delegated-ingest sources. - */ - -#include -#include -#include - -#include "pj_base/expected.hpp" - -namespace PJ { - -/// Versioned envelope that wraps a DataSource's config alongside host-owned -/// parser binding state. The source never sees `parser_binding`. -struct ConfigEnvelope { - static std::string pack(std::string_view source_config, std::string_view parser_binding = "{}") { - nlohmann::json envelope; - envelope["version"] = 1; - envelope["source_config"] = std::string(source_config); - envelope["parser_binding"] = std::string(parser_binding); - return envelope.dump(); - } - - struct Unpacked { - std::string source_config; - std::string parser_binding; - }; - - static Expected unpack(std::string_view envelope_json) { - auto j = nlohmann::json::parse(envelope_json, nullptr, false); - if (j.is_discarded()) { - return unexpected("invalid envelope JSON"); - } - if (!j.contains("version") || !j["version"].is_number_integer()) { - return unexpected("missing or invalid envelope version"); - } - if (j["version"].get() != 1) { - return unexpected("unsupported envelope version"); - } - if (!j.contains("source_config") || !j["source_config"].is_string()) { - return unexpected("missing source_config"); - } - - Unpacked result; - result.source_config = j["source_config"].get(); - result.parser_binding = - j.contains("parser_binding") && j["parser_binding"].is_string() ? j["parser_binding"].get() : "{}"; - return result; - } -}; - -} // namespace PJ diff --git a/pj_plugins/include/pj_plugins/host/message_parser_handle.hpp b/pj_plugins/include/pj_plugins/host/message_parser_handle.hpp index 896710b4..69dec663 100644 --- a/pj_plugins/include/pj_plugins/host/message_parser_handle.hpp +++ b/pj_plugins/include/pj_plugins/host/message_parser_handle.hpp @@ -134,23 +134,20 @@ class MessageParserHandle { return okStatus(); } - /// True when this parser exposes the 0.21 pure-functional C extension. - /// Older parsers return false and can be handled through the deprecated + /// True when this parser exposes the pure-functional C extension (v2, SDK + /// 0.22 and later). Older parsers return false and can be handled through the deprecated /// in-process C++ compatibility bridge until the next SDK major version. [[nodiscard]] bool supportsFunctionalParsing() const { - const auto extensions = functionalExtensions(); - return extensions.v2 != nullptr || extensions.v1 != nullptr; + return functionalExtension() != nullptr; } /// Parse one scalar record and synchronously deliver borrowed C ABI values /// to @p sink. The sink must copy names/string values it retains. [[nodiscard]] Status parseScalarsFunctional( Timestamp timestamp_ns, Span payload, const ScalarRecordSink& sink) const { - const auto extensions = functionalExtensions(); - if (extensions.v2 == nullptr && extensions.v1 == nullptr) { - return unexpected( - std::string("message parser does not expose ") + PJ_PARSER_FUNCTIONAL_EXTENSION_V2 + " or " + - PJ_PARSER_FUNCTIONAL_EXTENSION_V1); + const auto* extension = functionalExtension(); + if (extension == nullptr) { + return unexpected(std::string("message parser does not expose ") + PJ_PARSER_FUNCTIONAL_EXTENSION_V2); } if (!sink) { return unexpected(std::string("scalar record sink is empty")); @@ -164,8 +161,7 @@ class MessageParserHandle { }; PJ_error_t error{}; const PJ_bytes_view_t bytes{payload.data(), payload.size()}; - const auto parse_scalars = extensions.v2 != nullptr ? extensions.v2->parse_scalars : extensions.v1->parse_scalars; - if (!parse_scalars(ctx_, timestamp_ns, bytes, &abi_sink, &error)) { + if (!extension->parse_scalars(ctx_, timestamp_ns, bytes, &abi_sink, &error)) { return unexpected(sdk::errorToString(error)); } if (state.call_count != 1) { @@ -178,14 +174,12 @@ class MessageParserHandle { /// are decoded synchronously into host-owned SDK storage before returning. [[nodiscard]] Expected parseObjectFunctional( Timestamp timestamp_ns, Span payload) const { - const auto extensions = functionalExtensions(); - if (extensions.v2 == nullptr && extensions.v1 == nullptr) { - return unexpected( - std::string("message parser does not expose ") + PJ_PARSER_FUNCTIONAL_EXTENSION_V2 + " or " + - PJ_PARSER_FUNCTIONAL_EXTENSION_V1); + const auto* extension = functionalExtension(); + if (extension == nullptr) { + return unexpected(std::string("message parser does not expose ") + PJ_PARSER_FUNCTIONAL_EXTENSION_V2); } return parseObjectFunctionalAbi( - extensions, timestamp_ns, PJ_payload_t{.data = payload.data(), .size = payload.size(), .anchor = {}}, payload, + extension, timestamp_ns, PJ_payload_t{.data = payload.data(), .size = payload.size(), .anchor = {}}, payload, {}); } @@ -194,11 +188,9 @@ class MessageParserHandle { /// plugin's object has been serialized, including every failure path. [[nodiscard]] Expected parseObjectFunctional( Timestamp timestamp_ns, sdk::PayloadView payload) const { - const auto extensions = functionalExtensions(); - if (extensions.v2 == nullptr && extensions.v1 == nullptr) { - return unexpected( - std::string("message parser does not expose ") + PJ_PARSER_FUNCTIONAL_EXTENSION_V2 + " or " + - PJ_PARSER_FUNCTIONAL_EXTENSION_V1); + const auto* extension = functionalExtension(); + if (extension == nullptr) { + return unexpected(std::string("message parser does not expose ") + PJ_PARSER_FUNCTIONAL_EXTENSION_V2); } PJ_payload_t abi_payload{.data = payload.bytes.data(), .size = payload.bytes.size(), .anchor = {}}; @@ -208,7 +200,7 @@ class MessageParserHandle { abi_payload.anchor.ctx = held; abi_payload.anchor.release = [](void* ctx) noexcept { delete static_cast(ctx); }; } - return parseObjectFunctionalAbi(extensions, timestamp_ns, abi_payload, payload.bytes, std::move(splice_anchor)); + return parseObjectFunctionalAbi(extension, timestamp_ns, abi_payload, payload.bytes, std::move(splice_anchor)); } /// A priori classification of the bound schema. Tail-slot gated; when @@ -249,14 +241,9 @@ class MessageParserHandle { } private: - struct FunctionalExtensions { - const PJ_parser_functional_v2_t* v2 = nullptr; - const PJ_parser_functional_v1_t* v1 = nullptr; - }; - [[nodiscard]] Expected parseObjectFunctionalAbi( - FunctionalExtensions extensions, Timestamp timestamp_ns, PJ_payload_t payload, Span input_payload, - sdk::BufferAnchor input_anchor) const { + const PJ_parser_functional_v2_t* extension, Timestamp timestamp_ns, PJ_payload_t payload, + Span input_payload, sdk::BufferAnchor input_anchor) const { ObjectSinkState state{ .record = {}, .input_payload = input_payload, @@ -265,25 +252,14 @@ class MessageParserHandle { .call_count = 0, }; PJ_error_t error{}; - if (extensions.v2 != nullptr) { - PJ_parser_object_sink_v2_t abi_sink{ - .struct_size = sizeof(PJ_parser_object_sink_v2_t), - .ctx = &state, - .accept_object = acceptObject, - .accept_object_spliced = acceptObjectSpliced, - }; - if (!extensions.v2->parse_object(ctx_, timestamp_ns, payload, &abi_sink, &error)) { - return unexpected(sdk::errorToString(error)); - } - } else { - PJ_parser_object_sink_v1_t abi_sink{ - .struct_size = sizeof(PJ_parser_object_sink_v1_t), - .ctx = &state, - .accept_object = acceptObject, - }; - if (!extensions.v1->parse_object(ctx_, timestamp_ns, payload, &abi_sink, &error)) { - return unexpected(sdk::errorToString(error)); - } + PJ_parser_object_sink_v2_t abi_sink{ + .struct_size = sizeof(PJ_parser_object_sink_v2_t), + .ctx = &state, + .accept_object = acceptObject, + .accept_object_spliced = acceptObjectSpliced, + }; + if (!extension->parse_object(ctx_, timestamp_ns, payload, &abi_sink, &error)) { + return unexpected(sdk::errorToString(error)); } if (state.call_count != 1 || !state.record.has_value()) { return unexpected(std::string("functional parser returned success without exactly one canonical object")); @@ -304,20 +280,14 @@ class MessageParserHandle { uint32_t call_count = 0; }; - [[nodiscard]] FunctionalExtensions functionalExtensions() const { + [[nodiscard]] const PJ_parser_functional_v2_t* functionalExtension() const { const auto* v2 = static_cast(getPluginExtension(PJ_PARSER_FUNCTIONAL_EXTENSION_V2)); if (v2 == nullptr || v2->struct_size < PJ_PARSER_FUNCTIONAL_V2_MIN_SIZE || v2->parse_scalars == nullptr || v2->parse_object == nullptr) { - v2 = nullptr; - } - const auto* v1 = - static_cast(getPluginExtension(PJ_PARSER_FUNCTIONAL_EXTENSION_V1)); - if (v1 == nullptr || v1->struct_size < PJ_PARSER_FUNCTIONAL_V1_MIN_SIZE || v1->parse_scalars == nullptr || - v1->parse_object == nullptr) { - v1 = nullptr; + return nullptr; } - return {.v2 = v2, .v1 = v1}; + return v2; } static bool acceptScalarRecord( diff --git a/pj_plugins/include/pj_plugins/host/parser_route_resolver.hpp b/pj_plugins/include/pj_plugins/host/parser_route_resolver.hpp index 43ef7bc3..165dac46 100644 --- a/pj_plugins/include/pj_plugins/host/parser_route_resolver.hpp +++ b/pj_plugins/include/pj_plugins/host/parser_route_resolver.hpp @@ -83,7 +83,7 @@ struct ParserProbeDecision { using ParserProbeCallback = std::function; -/// Provider config supplied by the host's config-envelope layer. +/// Provider config supplied by the host. struct ParserProviderConfig { std::string json; std::string digest; diff --git a/pj_plugins/include/pj_plugins/host/service_registry_builder.hpp b/pj_plugins/include/pj_plugins/host/service_registry_builder.hpp index 2b7b81b2..b5d5e868 100644 --- a/pj_plugins/include/pj_plugins/host/service_registry_builder.hpp +++ b/pj_plugins/include/pj_plugins/host/service_registry_builder.hpp @@ -37,7 +37,7 @@ class ServiceRegistryBuilder { /// Register a service under @p name. Rejects null pointers and duplicate /// names (silent overwrite would mask configuration bugs). /// - /// @param name Canonical service name (e.g. "pj.colormap.v1"). + /// @param name Canonical service name (e.g. "pj.source_write.v1"). /// @param protocol_version The version of the service as implemented here. /// Consumers may request any version <= this value. /// @param service Fat pointer to the service. The builder does diff --git a/pj_plugins/include/pj_plugins/sdk/detail/message_parser_trampolines.hpp b/pj_plugins/include/pj_plugins/sdk/detail/message_parser_trampolines.hpp index 6436d284..201a77db 100644 --- a/pj_plugins/include/pj_plugins/sdk/detail/message_parser_trampolines.hpp +++ b/pj_plugins/include/pj_plugins/sdk/detail/message_parser_trampolines.hpp @@ -142,14 +142,6 @@ inline const void* MessageParserPluginBase::trampoline_get_plugin_extension(void const bool functional_route_available = self->bound_type_name_.empty() ? !self->handlers_.empty() : self->findSchemaHandler(self->bound_type_name_) != nullptr; - if (sv == PJ_PARSER_FUNCTIONAL_EXTENSION_V1 && functional_route_available) { - static const PJ_parser_functional_v1_t extension{ - .struct_size = sizeof(PJ_parser_functional_v1_t), - .parse_scalars = trampoline_parse_scalars_functional, - .parse_object = trampoline_parse_object_functional, - }; - return &extension; - } if (sv == PJ_PARSER_FUNCTIONAL_EXTENSION_V2 && functional_route_available) { static const PJ_parser_functional_v2_t extension{ .struct_size = sizeof(PJ_parser_functional_v2_t), @@ -164,41 +156,6 @@ inline const void* MessageParserPluginBase::trampoline_get_plugin_extension(void } } -inline bool MessageParserPluginBase::trampoline_parse_scalars_functional( - void* ctx, int64_t timestamp_ns, PJ_bytes_view_t payload, const PJ_parser_scalar_sink_v1_t* sink, - PJ_error_t* out_error) noexcept { - if (ctx == nullptr) { - storeError(out_error, 2, "plugin", "parse_scalars called with null plugin context"); - return false; - } - auto* self = static_cast(ctx); - if (sink == nullptr || sink->struct_size < PJ_PARSER_SCALAR_SINK_V1_MIN_SIZE || sink->accept_record == nullptr) { - self->storeError(out_error, 2, "plugin", "parse_scalars called with invalid scalar sink"); - return false; - } - if (payload.data == nullptr && payload.size != 0) { - self->storeError(out_error, 2, "plugin", "parse_scalars called with invalid payload"); - return false; - } - - try { - auto record = self->parseScalars(timestamp_ns, Span(payload.data, payload.size)); - if (!record) { - self->storeError(out_error, 1, "plugin", std::move(record).error()); - return false; - } - const auto fields = sdk::toAbiNamed(Span(record->fields)); - return sink->accept_record( - sink->ctx, record->ts.has_value(), record->ts.value_or(0), fields.data(), fields.size(), out_error); - } catch (const std::exception& e) { - self->storeError(out_error, 1, "plugin", std::string("parse_scalars threw: ") + e.what()); - return false; - } catch (...) { - self->storeError(out_error, 1, "plugin", "unknown exception in parse_scalars"); - return false; - } -} - inline bool MessageParserPluginBase::trampoline_parse_scalars_functional_v2( void* ctx, int64_t timestamp_ns, PJ_bytes_view_t payload, const PJ_parser_scalar_sink_v1_t* sink, PJ_error_t* out_error) noexcept { @@ -247,68 +204,6 @@ inline bool MessageParserPluginBase::trampoline_parse_scalars_functional_v2( } } -inline bool MessageParserPluginBase::trampoline_parse_object_functional( - void* ctx, int64_t timestamp_ns, PJ_payload_t payload, const PJ_parser_object_sink_v1_t* sink, - PJ_error_t* out_error) noexcept { - if (payload.anchor.ctx != nullptr && payload.anchor.release == nullptr) { - storeError(out_error, 2, "plugin", "parse_object payload anchor has context without release callback"); - return false; - } - - // parse_object consumes the one C anchor reference on every path. Copies of - // this shared_ptr remain entirely inside the plugin DSO and keep the host - // payload alive until the returned object has been serialized. - std::shared_ptr payload_owner; - if (payload.anchor.release != nullptr) { - try { - payload_owner = std::shared_ptr(payload.anchor.ctx, payload.anchor.release); - } catch (const std::exception& e) { - storeError(out_error, 1, "plugin", std::string("payload anchor adoption failed: ") + e.what()); - return false; - } catch (...) { - storeError(out_error, 1, "plugin", "unknown exception adopting payload anchor"); - return false; - } - } - if (ctx == nullptr) { - storeError(out_error, 2, "plugin", "parse_object called with null plugin context"); - return false; - } - auto* self = static_cast(ctx); - if (sink == nullptr || sink->struct_size < PJ_PARSER_OBJECT_SINK_V1_MIN_SIZE || sink->accept_object == nullptr) { - self->storeError(out_error, 2, "plugin", "parse_object called with invalid object sink"); - return false; - } - if (payload.data == nullptr && payload.size != 0) { - self->storeError(out_error, 2, "plugin", "parse_object called with invalid payload"); - return false; - } - - try { - sdk::PayloadView payload_view{Span(payload.data, payload.size), std::move(payload_owner)}; - auto record = self->parseObject(timestamp_ns, std::move(payload_view)); - if (!record) { - self->storeError(out_error, 1, "plugin", std::move(record).error()); - return false; - } - const auto object_type = sdk::typeOf(record->object); - auto canonical_wire = serializeBuiltinObject(record->object); - if (!canonical_wire) { - self->storeError(out_error, 1, "plugin", std::move(canonical_wire).error()); - return false; - } - return sink->accept_object( - sink->ctx, record->ts.has_value(), record->ts.value_or(0), static_cast(object_type), - PJ_bytes_view_t{canonical_wire->data(), canonical_wire->size()}, out_error); - } catch (const std::exception& e) { - self->storeError(out_error, 1, "plugin", std::string("parse_object threw: ") + e.what()); - return false; - } catch (...) { - self->storeError(out_error, 1, "plugin", "unknown exception in parse_object"); - return false; - } -} - inline bool MessageParserPluginBase::trampoline_parse_object_functional_v2( void* ctx, int64_t timestamp_ns, PJ_payload_t payload, const PJ_parser_object_sink_v2_t* sink, PJ_error_t* out_error) noexcept { @@ -319,7 +214,7 @@ inline bool MessageParserPluginBase::trampoline_parse_object_functional_v2( return false; } - // Functional v2 preserves v1's one-reference ownership rule. Builtin + // Functional v2 consumes exactly one payload anchor reference. Builtin // handlers materialize complete canonical wire in this revision, so only // accept_object is invoked; accept_object_spliced remains available to // module-aware or future handlers. diff --git a/pj_plugins/include/pj_plugins/sdk/message_parser_plugin_base.hpp b/pj_plugins/include/pj_plugins/sdk/message_parser_plugin_base.hpp index 89deda75..62fc43f8 100644 --- a/pj_plugins/include/pj_plugins/sdk/message_parser_plugin_base.hpp +++ b/pj_plugins/include/pj_plugins/sdk/message_parser_plugin_base.hpp @@ -86,10 +86,10 @@ struct MessageParserPluginBaseLayoutSentinel; /** * Base class for MessageParser plugins (protocol v4). * - * TRANSITIONAL ABI LAYOUT CONTRACT: SDK 0.21 hosts use the - * `pj.parser_functional.v1` C extension and never cast a newly-built plugin's + * TRANSITIONAL ABI LAYOUT CONTRACT: hosts use the + * `pj.parser_functional.v2` C extension and never cast a newly-built plugin's * opaque context to this class. Hosts may retain a deprecated direct-C++ - * bridge only for pre-0.21 plugins that do not expose the extension. Therefore + * bridge only for pre-0.22 plugins that do not expose the extension. Therefore * the existing member prefix remains frozen within PJ_ABI_VERSION 5: members * must not move, and additions are tail-only. SDK 1.0 may remove that bridge * and this layout constraint together. @@ -183,7 +183,7 @@ class MessageParserPluginBase { /// This entry point exists for compatibility with the legacy v4 ingest /// path (host calls parser.parse() directly to push fields to writeHost). /// New host code invokes the pure-functional pair through - /// MessageParserHandle and `pj.parser_functional.v1`, never by casting the + /// MessageParserHandle and `pj.parser_functional.v2`, never by casting the /// opaque plugin context. The pair enables lazy materialization because the /// caller (DataSource / app) receives the result rather than a side effect. /// Once every plugin migrates to SchemaHandler, parse() can be deprecated. @@ -308,8 +308,8 @@ class MessageParserPluginBase { /// Return a pointer to a static plugin-exposed extension for @p id, or /// nullptr if unknown. Default returns nullptr. The SDK reserves - /// `pj.parser_route_claims.v1` plus `pj.parser_functional.v1` and v2. Route - /// claims are always advertised; both functional revisions are advertised + /// `pj.parser_route_claims.v1` and `pj.parser_functional.v2`. Route + /// claims are always advertised; functional v2 is advertised /// automatically after at least one SchemaHandler has been registered. virtual const void* pluginExtension(std::string_view id) { (void)id; @@ -425,15 +425,9 @@ class MessageParserPluginBase { void* ctx, int64_t timestamp_ns, PJ_bytes_view_t payload, PJ_error_t* out_error) noexcept; PJ_MESSAGE_PARSER_DSO_LOCAL static const void* trampoline_get_plugin_extension( void* ctx, PJ_string_view_t id) noexcept; - PJ_MESSAGE_PARSER_DSO_LOCAL static bool trampoline_parse_scalars_functional( - void* ctx, int64_t timestamp_ns, PJ_bytes_view_t payload, const PJ_parser_scalar_sink_v1_t* sink, - PJ_error_t* out_error) noexcept; PJ_MESSAGE_PARSER_DSO_LOCAL static bool trampoline_parse_scalars_functional_v2( void* ctx, int64_t timestamp_ns, PJ_bytes_view_t payload, const PJ_parser_scalar_sink_v1_t* sink, PJ_error_t* out_error) noexcept; - PJ_MESSAGE_PARSER_DSO_LOCAL static bool trampoline_parse_object_functional( - void* ctx, int64_t timestamp_ns, PJ_payload_t payload, const PJ_parser_object_sink_v1_t* sink, - PJ_error_t* out_error) noexcept; PJ_MESSAGE_PARSER_DSO_LOCAL static bool trampoline_parse_object_functional_v2( void* ctx, int64_t timestamp_ns, PJ_payload_t payload, const PJ_parser_object_sink_v2_t* sink, PJ_error_t* out_error) noexcept; diff --git a/pj_plugins/tests/message_parser_functional_extension_test.cpp b/pj_plugins/tests/message_parser_functional_extension_test.cpp index c5fc976d..43ce2c2f 100644 --- a/pj_plugins/tests/message_parser_functional_extension_test.cpp +++ b/pj_plugins/tests/message_parser_functional_extension_test.cpp @@ -140,7 +140,7 @@ bool emitNoScalars(void*, int64_t, PJ_bytes_view_t, const PJ_parser_scalar_sink_ return true; } -bool emitNoObject(void*, int64_t, PJ_payload_t, const PJ_parser_object_sink_v1_t*, PJ_error_t*) noexcept { +bool emitNoObject(void*, int64_t, PJ_payload_t, const PJ_parser_object_sink_v2_t*, PJ_error_t*) noexcept { return true; } @@ -155,7 +155,7 @@ bool emitTwoScalarRecords( } bool emitUnknownObject( - void*, int64_t, PJ_payload_t, const PJ_parser_object_sink_v1_t* sink, PJ_error_t* out_error) noexcept { + void*, int64_t, PJ_payload_t, const PJ_parser_object_sink_v2_t* sink, PJ_error_t* out_error) noexcept { if (sink == nullptr || sink->accept_object == nullptr) { return false; } @@ -163,14 +163,6 @@ bool emitUnknownObject( return sink->accept_object(sink->ctx, false, 0, 999, PJ_bytes_view_t{&byte, sizeof(byte)}, out_error); } -bool emitImageV1(void*, int64_t, PJ_payload_t, const PJ_parser_object_sink_v1_t* sink, PJ_error_t* out_error) noexcept { - static constexpr std::array kImageWire{0x10, 0x01}; - return sink != nullptr && sink->accept_object != nullptr && - sink->accept_object( - sink->ctx, false, 0, PJ_BUILTIN_OBJECT_TYPE_IMAGE, PJ_bytes_view_t{kImageWire.data(), kImageWire.size()}, - out_error); -} - bool emitSplicedPointCloudV2( void*, int64_t, PJ_payload_t, const PJ_parser_object_sink_v2_t* sink, PJ_error_t* out_error) noexcept { static constexpr std::array kPointCloudWire{0x10, 0x01}; @@ -211,10 +203,10 @@ bool emitMismatchedImageV2( out_error); } -template +template const PJ_message_parser_vtable_t* adversarialVtable() { - static const PJ_parser_functional_v1_t extension{ - .struct_size = sizeof(PJ_parser_functional_v1_t), + static const PJ_parser_functional_v2_t extension{ + .struct_size = sizeof(PJ_parser_functional_v2_t), .parse_scalars = ParseScalars, .parse_object = ParseObject, }; @@ -222,7 +214,7 @@ const PJ_message_parser_vtable_t* adversarialVtable() { PJ_message_parser_vtable_t copy = *parserVtable(); copy.get_plugin_extension = [](void*, PJ_string_view_t id) noexcept -> const void* { const std::string_view name{id.data == nullptr ? "" : id.data, id.size}; - return name == PJ_PARSER_FUNCTIONAL_EXTENSION_V1 ? &extension : nullptr; + return name == PJ_PARSER_FUNCTIONAL_EXTENSION_V2 ? &extension : nullptr; }; return copy; }(); @@ -262,22 +254,20 @@ TEST(MessageParserFunctionalExtension, NewlyBuiltParserExposesStableExtensionAut ASSERT_TRUE(handle.valid()); EXPECT_TRUE(handle.supportsFunctionalParsing()); const auto* extension = - static_cast(handle.getPluginExtension(PJ_PARSER_FUNCTIONAL_EXTENSION_V1)); + static_cast(handle.getPluginExtension(PJ_PARSER_FUNCTIONAL_EXTENSION_V2)); ASSERT_NE(extension, nullptr); - EXPECT_GE(extension->struct_size, PJ_PARSER_FUNCTIONAL_V1_MIN_SIZE); + EXPECT_GE(extension->struct_size, PJ_PARSER_FUNCTIONAL_V2_MIN_SIZE); + // The retired v1 id is never answered by a newly built parser. + EXPECT_EQ(handle.getPluginExtension("pj.parser_functional.v1"), nullptr); } -TEST(MessageParserFunctionalExtension, NewlyBuiltParserExposesV1AndV2AndV2EmitsFullCanonicalWire) { +TEST(MessageParserFunctionalExtension, NewlyBuiltParserExposesV2AndV2EmitsFullCanonicalWire) { PJ::MessageParserHandle handle(parserVtable()); ASSERT_TRUE(handle.bindSchema(kSchema, {})); - const auto* v1 = - static_cast(handle.getPluginExtension(PJ_PARSER_FUNCTIONAL_EXTENSION_V1)); const auto* v2 = static_cast(handle.getPluginExtension(PJ_PARSER_FUNCTIONAL_EXTENSION_V2)); - ASSERT_NE(v1, nullptr); ASSERT_NE(v2, nullptr); - EXPECT_GE(v1->struct_size, PJ_PARSER_FUNCTIONAL_V1_MIN_SIZE); EXPECT_GE(v2->struct_size, PJ_PARSER_FUNCTIONAL_V2_MIN_SIZE); EXPECT_NE(v2->parse_scalars, nullptr); @@ -446,12 +436,43 @@ TEST(MessageParserFunctionalExtension, ObjectRouteReturnsAHostOwnedTypedValue) { EXPECT_EQ(image->data[3], 6U); } -TEST(MessageParserFunctionalExtension, HostFallsBackToV1WhenV2IsAbsent) { - PJ::MessageParserHandle handle(adversarialVtable()); +TEST(MessageParserFunctionalExtension, HostTreatsRetiredV1OnlyParserAsHavingNoFunctionalRoute) { + // A parser built against SDK 0.21 through 1.0.0-dev.1 answers the retired + // "pj.parser_functional.v1" id with a well-formed table. The host must not + // negotiate it: a restored fallback would call these entry points. + struct RetiredV1Table { + uint32_t struct_size; + PJ_parser_parse_scalars_fn_t parse_scalars; + bool (*parse_object)(void*, int64_t, PJ_payload_t, const void*, PJ_error_t*) noexcept; + }; + static int retired_calls = 0; + retired_calls = 0; + static const PJ_message_parser_vtable_t vtable = [] { + PJ_message_parser_vtable_t copy = *parserVtable(); + copy.get_plugin_extension = [](void*, PJ_string_view_t id) noexcept -> const void* { + static const RetiredV1Table old_table{ + sizeof(RetiredV1Table), + [](void*, int64_t, PJ_bytes_view_t, const PJ_parser_scalar_sink_v1_t*, PJ_error_t*) noexcept { + ++retired_calls; + return true; + }, + [](void*, int64_t, PJ_payload_t, const void*, PJ_error_t*) noexcept { + ++retired_calls; + return true; + }, + }; + const std::string_view name{id.data == nullptr ? "" : id.data, id.size}; + return name == "pj.parser_functional.v1" ? &old_table : nullptr; + }; + return copy; + }(); + PJ::MessageParserHandle handle(&vtable); ASSERT_TRUE(handle.bindSchema(kSchema, {})); + EXPECT_FALSE(handle.supportsFunctionalParsing()); auto record = handle.parseObjectFunctional(0, PJ::Span{}); - ASSERT_TRUE(record.has_value()) << record.error(); - EXPECT_EQ(PJ::sdk::typeOf(record->object), PJ::sdk::BuiltinObjectType::kImage); + ASSERT_FALSE(record.has_value()); + EXPECT_NE(record.error().find(PJ_PARSER_FUNCTIONAL_EXTENSION_V2), std::string::npos); + EXPECT_EQ(retired_calls, 0); } TEST(MessageParserFunctionalExtension, HostV2PathReconstructsEligibleSplices) { @@ -536,7 +557,7 @@ TEST(MessageParserFunctionalExtension, NewlyBuiltLegacyStyleParserDoesNotFalsely const auto status = handle.parseScalarsFunctional( 0, {}, [](std::optional, PJ::Span) { return PJ::okStatus(); }); EXPECT_FALSE(status); - EXPECT_NE(status.error().find(PJ_PARSER_FUNCTIONAL_EXTENSION_V1), std::string::npos); + EXPECT_NE(status.error().find(PJ_PARSER_FUNCTIONAL_EXTENSION_V2), std::string::npos); } TEST(MessageParserFunctionalExtension, HandlerRegisteredDuringBindEnablesExtensionWithoutCapabilityCaching) { @@ -561,7 +582,7 @@ TEST(MessageParserFunctionalExtension, MixedModelParserAdvertisesOnlyForHandledS const auto status = unhandled.parseScalarsFunctional( 0, {}, [](std::optional, PJ::Span) { return PJ::okStatus(); }); EXPECT_FALSE(status); - EXPECT_NE(status.error().find(PJ_PARSER_FUNCTIONAL_EXTENSION_V1), std::string::npos); + EXPECT_NE(status.error().find(PJ_PARSER_FUNCTIONAL_EXTENSION_V2), std::string::npos); } TEST(MessageParserFunctionalExtension, RebindingToAnUnhandledSchemaWithdrawsTheFunctionalRoute) { @@ -573,18 +594,16 @@ TEST(MessageParserFunctionalExtension, RebindingToAnUnhandledSchemaWithdrawsTheF EXPECT_FALSE(handle.supportsFunctionalParsing()); } -TEST(MessageParserFunctionalExtension, SchemaGatingWithdrawsV2AlongsideV1) { - // v2 shares v1's advertisement gate. A host prefers v2, so a v2 that stayed - // advertised for a schema only legacy parse() implements would send every - // message down a route parse_object/parse_scalars can only reject. +TEST(MessageParserFunctionalExtension, SchemaGatingWithdrawsV2) { + // A v2 that stayed advertised for a schema only legacy parse() implements + // would send every message down a route parse_object/parse_scalars can only + // reject. PJ::MessageParserHandle handled(parserVtable()); ASSERT_TRUE(handled.bindSchema(kSchema, {})); - EXPECT_NE(handled.getPluginExtension(PJ_PARSER_FUNCTIONAL_EXTENSION_V1), nullptr); EXPECT_NE(handled.getPluginExtension(PJ_PARSER_FUNCTIONAL_EXTENSION_V2), nullptr); PJ::MessageParserHandle unhandled(parserVtable()); ASSERT_TRUE(unhandled.bindSchema("example/Unhandled", {})); - EXPECT_EQ(unhandled.getPluginExtension(PJ_PARSER_FUNCTIONAL_EXTENSION_V1), nullptr); EXPECT_EQ(unhandled.getPluginExtension(PJ_PARSER_FUNCTIONAL_EXTENSION_V2), nullptr); } @@ -595,7 +614,7 @@ TEST(MessageParserFunctionalExtension, LegacyParserWithoutExtensionRemainsDetect const auto status = handle.parseScalarsFunctional( 0, {}, [](std::optional, PJ::Span) { return PJ::okStatus(); }); EXPECT_FALSE(status); - EXPECT_NE(status.error().find(PJ_PARSER_FUNCTIONAL_EXTENSION_V1), std::string::npos); + EXPECT_NE(status.error().find(PJ_PARSER_FUNCTIONAL_EXTENSION_V2), std::string::npos); } TEST(MessageParserFunctionalExtension, HostRejectsSuccessWithoutExactlyOneSinkCall) { @@ -636,7 +655,7 @@ TEST(MessageParserFunctionalExtension, ProviderRejectsMalformedCallerOwnedSinkBe PJ::MessageParserHandle handle(parserVtable()); ASSERT_TRUE(handle.bindSchema(kSchema, {})); const auto* extension = - static_cast(handle.getPluginExtension(PJ_PARSER_FUNCTIONAL_EXTENSION_V1)); + static_cast(handle.getPluginExtension(PJ_PARSER_FUNCTIONAL_EXTENSION_V2)); ASSERT_NE(extension, nullptr); PJ_parser_scalar_sink_v1_t undersized_sink{}; @@ -645,8 +664,8 @@ TEST(MessageParserFunctionalExtension, ProviderRejectsMalformedCallerOwnedSinkBe EXPECT_FALSE(extension->parse_scalars(handle.context(), 0, PJ_bytes_view_t{}, &undersized_sink, &error)); EXPECT_NE(std::string_view(error.message).find("scalar sink"), std::string_view::npos); - PJ_parser_object_sink_v1_t undersized_object_sink{}; - undersized_object_sink.struct_size = offsetof(PJ_parser_object_sink_v1_t, accept_object); + PJ_parser_object_sink_v2_t undersized_object_sink{}; + undersized_object_sink.struct_size = offsetof(PJ_parser_object_sink_v2_t, accept_object_spliced); error = {}; EXPECT_FALSE(extension->parse_object(handle.context(), 0, PJ_payload_t{}, &undersized_object_sink, &error)); EXPECT_NE(std::string_view(error.message).find("object sink"), std::string_view::npos); @@ -656,7 +675,7 @@ TEST(MessageParserFunctionalExtension, ObjectPayloadAnchorIsReleasedExactlyOnceA PJ::MessageParserHandle handle(parserVtable()); ASSERT_TRUE(handle.bindSchema(kSchema, {})); const auto* extension = - static_cast(handle.getPluginExtension(PJ_PARSER_FUNCTIONAL_EXTENSION_V1)); + static_cast(handle.getPluginExtension(PJ_PARSER_FUNCTIONAL_EXTENSION_V2)); ASSERT_NE(extension, nullptr); int releases = 0; @@ -670,10 +689,12 @@ TEST(MessageParserFunctionalExtension, ObjectPayloadAnchorIsReleasedExactlyOnceA .release = [](void* ctx) { ++*static_cast(ctx); }, }, }; - PJ_parser_object_sink_v1_t sink{ - .struct_size = sizeof(PJ_parser_object_sink_v1_t), + PJ_parser_object_sink_v2_t sink{ + .struct_size = sizeof(PJ_parser_object_sink_v2_t), .ctx = nullptr, .accept_object = [](void*, bool, int64_t, uint16_t, PJ_bytes_view_t, PJ_error_t*) noexcept { return true; }, + .accept_object_spliced = [](void*, bool, int64_t, uint16_t, PJ_bytes_view_t, uint32_t, uint64_t, uint64_t, + PJ_error_t*) noexcept { return true; }, }; PJ_error_t error{}; @@ -682,8 +703,8 @@ TEST(MessageParserFunctionalExtension, ObjectPayloadAnchorIsReleasedExactlyOnceA PJ::MessageParserHandle throwing_handle(parserVtable()); ASSERT_TRUE(throwing_handle.bindSchema(kSchema, {})); - const auto* throwing_extension = static_cast( - throwing_handle.getPluginExtension(PJ_PARSER_FUNCTIONAL_EXTENSION_V1)); + const auto* throwing_extension = static_cast( + throwing_handle.getPluginExtension(PJ_PARSER_FUNCTIONAL_EXTENSION_V2)); ASSERT_NE(throwing_extension, nullptr); releases = 0; error = {}; diff --git a/pj_plugins/tests/source_dialog_integration_test.cpp b/pj_plugins/tests/source_dialog_integration_test.cpp index 633a9e64..0aaf55bb 100644 --- a/pj_plugins/tests/source_dialog_integration_test.cpp +++ b/pj_plugins/tests/source_dialog_integration_test.cpp @@ -11,7 +11,6 @@ #include #include "detail/library_loader.hpp" -#include "pj_plugins/host/config_envelope.hpp" #include "pj_plugins/host/data_source_library.hpp" #include "pj_plugins/host/dialog_handle.hpp" @@ -341,32 +340,4 @@ TEST(SourceDialogIntegration, NoDialogPluginReturnsNull) { EXPECT_FALSE(dialog_vt); } -// --- Test 10: Config envelope --- - -TEST(SourceDialogIntegration, ConfigEnvelopeRoundTrip) { - std::string source_cfg = R"({"host":"localhost","port":9090})"; - std::string parser_binding = R"({"parser":"json"})"; - - std::string packed = PJ::ConfigEnvelope::pack(source_cfg, parser_binding); - auto unpacked = PJ::ConfigEnvelope::unpack(packed); - ASSERT_TRUE(unpacked) << unpacked.error(); - EXPECT_EQ(unpacked->source_config, source_cfg); - EXPECT_EQ(unpacked->parser_binding, parser_binding); -} - -TEST(SourceDialogIntegration, ConfigEnvelopeDefaultParserBinding) { - std::string source_cfg = R"({"host":"example.com"})"; - std::string packed = PJ::ConfigEnvelope::pack(source_cfg); - auto unpacked = PJ::ConfigEnvelope::unpack(packed); - ASSERT_TRUE(unpacked) << unpacked.error(); - EXPECT_EQ(unpacked->source_config, source_cfg); - EXPECT_EQ(unpacked->parser_binding, "{}"); -} - -TEST(SourceDialogIntegration, ConfigEnvelopeRejectsInvalid) { - EXPECT_FALSE(PJ::ConfigEnvelope::unpack("not json")); - EXPECT_FALSE(PJ::ConfigEnvelope::unpack(R"({"version":99})")); - EXPECT_FALSE(PJ::ConfigEnvelope::unpack(R"({"version":1})")); -} - } // namespace diff --git a/recipe.yaml b/recipe.yaml index 64c74f87..a8798ba1 100644 --- a/recipe.yaml +++ b/recipe.yaml @@ -38,7 +38,7 @@ requirements: - fmt 12.* - fast_float 8.* run: - # nlohmann_json appears in PUBLIC installed headers (config_envelope.hpp) and is + # nlohmann_json appears in PUBLIC installed headers (pj_plugins/sdk/streaming_source.hpp) and is # find_dependency()'d by the installed CMake config, so consumers need it at use # time. Constrain to the 3.x line built against (header-only API breaks at majors). - nlohmann_json >=3.12,<4 diff --git a/test_sdk_install.sh b/test_sdk_install.sh index eb9c3ec8..ba3598da 100755 --- a/test_sdk_install.sh +++ b/test_sdk_install.sh @@ -76,7 +76,7 @@ cmake --build "$CONSUMER_BUILD_DIR" -j "$(nproc)" echo "" echo "--- Step 4: Smoke-test find_package COMPONENTS ---" -for comp in base plugin_sdk plugin_host source descriptor_import_support; do +for comp in base plugin_sdk plugin_host source; do COMP_DIR="$(mktemp -d)" cat > "$COMP_DIR/CMakeLists.txt" <