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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -124,11 +124,18 @@ inline const void* MessageParserPluginBase::trampoline_get_plugin_extension(void
auto* self = static_cast<MessageParserPluginBase*>(ctx);
try {
std::string_view sv = id.data == nullptr ? std::string_view{} : std::string_view(id.data, id.size);
// Do not falsely advertise the functional route for a newly rebuilt
// plugin that still implements only legacy parse(). Schema handlers may
// be registered in the constructor or bindSchema(); hosts query again
// after binding and do not cache an earlier absence.
if (sv == PJ_PARSER_FUNCTIONAL_EXTENSION_V1 && !self->handlers_.empty()) {
// Once a schema is bound, advertise the functional route only if THIS
// schema has a handler. A mixed-model plugin may register handlers for
// some schemas and keep legacy parse() for the rest; gating on "any
// handler exists" would make the host prefer a functional route that
// parseScalars/parseObject then reject for the unhandled schema. Before
// binding, any registered handler still advertises the capability, since
// no schema-specific answer exists yet and hosts query again after
// binding without caching an earlier absence.
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,
Expand Down
43 changes: 43 additions & 0 deletions pj_plugins/tests/message_parser_functional_extension_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,23 @@ class CustomExtensionParser final : public PJ::MessageParserPluginBase {
int marker_ = 17;
};

/// Registers a handler for one schema and keeps legacy parse() for every
/// other schema — the mixed model the parse() doc-comment sanctions.
class MixedModelParser final : public PJ::MessageParserPluginBase {
public:
MixedModelParser() {
PJ::sdk::SchemaHandler handler;
handler.parse_scalars = [](PJ::Timestamp, PJ::Span<const uint8_t>) -> PJ::Expected<PJ::sdk::ScalarRecord> {
return PJ::sdk::ScalarRecord{};
};
registerSchemaHandler(kSchema, std::move(handler));
}

PJ::Status parse(PJ::Timestamp, PJ::Span<const uint8_t>) override {
return PJ::okStatus();
}
};

class BindRegisteredParser final : public PJ::MessageParserPluginBase {
public:
PJ::Status bindSchema(std::string_view type_name, PJ::Span<const uint8_t> schema) override {
Expand Down Expand Up @@ -275,6 +292,32 @@ TEST(MessageParserFunctionalExtension, HandlerRegisteredDuringBindEnablesExtensi
EXPECT_TRUE(handle.supportsFunctionalParsing());
}

TEST(MessageParserFunctionalExtension, MixedModelParserAdvertisesOnlyForHandledSchemas) {
PJ::MessageParserHandle handled(parserVtable<MixedModelParser>());
ASSERT_TRUE(handled.bindSchema(kSchema, {}));
EXPECT_TRUE(handled.supportsFunctionalParsing());

// The same plugin bound to a schema it only implements through legacy
// parse() must not claim the functional route, or the host would take a
// route that parseScalars/parseObject can only reject.
PJ::MessageParserHandle unhandled(parserVtable<MixedModelParser>());
ASSERT_TRUE(unhandled.bindSchema("example/Unhandled", {}));
EXPECT_FALSE(unhandled.supportsFunctionalParsing());
const auto status = unhandled.parseScalarsFunctional(
0, {}, [](std::optional<PJ::Timestamp>, PJ::Span<const PJ_named_field_value_t>) { return PJ::okStatus(); });
EXPECT_FALSE(status);
EXPECT_NE(status.error().find(PJ_PARSER_FUNCTIONAL_EXTENSION_V1), std::string::npos);
}

TEST(MessageParserFunctionalExtension, RebindingToAnUnhandledSchemaWithdrawsTheFunctionalRoute) {
PJ::MessageParserHandle handle(parserVtable<MixedModelParser>());
ASSERT_TRUE(handle.bindSchema(kSchema, {}));
ASSERT_TRUE(handle.supportsFunctionalParsing());

ASSERT_TRUE(handle.bindSchema("example/Unhandled", {}));
EXPECT_FALSE(handle.supportsFunctionalParsing());
}

TEST(MessageParserFunctionalExtension, LegacyParserWithoutExtensionRemainsDetectable) {
PJ::MessageParserHandle handle(legacyStyleVtable());

Expand Down
Loading