This is an automated email from the ASF dual-hosted git repository. lordgamez pushed a commit to branch MINIFICPP-2458 in repository https://gitbox.apache.org/repos/asf/nifi-minifi-cpp.git
commit 5971b19cc5cc453a7bbab2513ef890c9dd0ea23c Author: Gabor Gyimesi <[email protected]> AuthorDate: Thu Jul 10 15:47:19 2025 +0200 Review update --- PROCESSORS.md | 18 +++++--- .../processors/EvaluateJsonPath.cpp | 10 +++-- .../processors/EvaluateJsonPath.h | 8 +++- .../tests/unit/EvaluateJsonPathTests.cpp | 52 ++++++++++------------ 4 files changed, 48 insertions(+), 40 deletions(-) diff --git a/PROCESSORS.md b/PROCESSORS.md index dfeef9556..d5815d89c 100644 --- a/PROCESSORS.md +++ b/PROCESSORS.md @@ -638,12 +638,18 @@ Evaluates one or more JsonPath expressions against the content of a FlowFile. Th In the list below, the names of required properties appear in bold. Any other properties (not in bold) are considered optional. The table also indicates any default values, and whether a property supports the NiFi Expression Language. -| Name | Default Value | Allowable Values | Description | -|-------------------------------|--------------------|-----------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| **Destination** | flowfile-attribute | flowfile-content<br/>flowfile-attribute | Indicates whether the results of the JsonPath evaluation are written to the FlowFile content or a FlowFile attribute. If using attribute, must specify the Attribute Name property. If set to flowfile-content, only one JsonPath may be specified, and the property name is ignored. | -| **Null Value Representation** | empty string | empty string<br/>the string 'null' | Indicates the desired representation of JSON Path expressions resulting in a null value. | -| **Path Not Found Behavior** | ignore | warn<br/>ignore<br/>skip | Indicates how to handle missing JSON path expressions when destination is set to 'flowfile-attribute'. Selecting 'warn' will generate a warning when a JSON path expression is not found. Selecting 'skip' will omit attributes for any unmatched JSON path expressions. | -| **Return Type** | auto-detect | auto-detect<br/>json<br/>scalar | Indicates the desired return type of the JSON Path expressions. Selecting 'auto-detect' will set the return type to 'json' for a Destination of 'flowfile-content', and 'scalar' for a Destination of 'flowfile-attribute'. | +| Name | Default Value | Allowable Values | Description | +|-------------------------------|--------------------|-----------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| **Destination** | flowfile-attribute | flowfile-content<br/>flowfile-attribute | Indicates whether the results of the JsonPath evaluation are written to the FlowFile content or a FlowFile attribute. | +| **Null Value Representation** | empty string | empty string<br/>the string 'null' | Indicates the desired representation of JSON Path expressions resulting in a null value. | +| **Path Not Found Behavior** | ignore | warn<br/>ignore<br/>skip | Indicates how to handle missing JSON path expressions when destination is set to 'flowfile-attribute'. Selecting 'warn' will generate a warning when a JSON path expression is not found. Selecting 'skip' will omit attributes for any unmatched JSON path expressions. | +| **Return Type** | auto-detect | auto-detect<br/>json<br/>scalar | Indicates the desired return type of the JSON Path expressions. Selecting 'auto-detect' will set the return type to 'json' for a Destination of 'flowfile-content', and 'scalar' for a Destination of 'flowfile-attribute'. | + +### Dynamic Properties + +| Name | Value | Description | +|-------------------|---------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Evaluation Result | JsonPath expression to evaluate | Dynamic property values are evaluated as JsonPaths. In case of 'flowfile-conent' destination, only one dynamic property with JsonPath may be specified, in this case the name of the property is ignored. In case of 'flowfile-attribute' destination, the result of the JsonPath evaluation is written to the 'Evaluation Result' property.<br/>**Supports Expression Language: true** | ### Relationships diff --git a/extensions/standard-processors/processors/EvaluateJsonPath.cpp b/extensions/standard-processors/processors/EvaluateJsonPath.cpp index 4b39572da..a8e553f7c 100644 --- a/extensions/standard-processors/processors/EvaluateJsonPath.cpp +++ b/extensions/standard-processors/processors/EvaluateJsonPath.cpp @@ -43,6 +43,9 @@ void EvaluateJsonPath::initialize() { } void EvaluateJsonPath::onSchedule(core::ProcessContext& context, core::ProcessSessionFactory&) { + if (getDynamicProperties().size() < 1) { + throw Exception(PROCESS_SCHEDULE_EXCEPTION, "At least one dynamic property must be specified with a valid JSON path expression"); + } destination_ = utils::parseEnumProperty<evaluate_json_path::DestinationType>(context, EvaluateJsonPath::Destination); if (destination_ == evaluate_json_path::DestinationType::FlowFileContent && getDynamicProperties().size() > 1) { throw Exception(PROCESS_SCHEDULE_EXCEPTION, "Only one dynamic property is allowed for JSON path when destination is set to flowfile-content"); @@ -89,14 +92,15 @@ void EvaluateJsonPath::writeQueryResult(core::ProcessSession& session, core::Flo } } -void EvaluateJsonPath::onTrigger(core::ProcessContext&, core::ProcessSession& session) { +void EvaluateJsonPath::onTrigger(core::ProcessContext& context, core::ProcessSession& session) { auto flow_file = session.get(); if (!flow_file) { + context.yield(); + logger_->log_debug("No FlowFile available, yielding"); return; } - const auto flow_file_read_result = session.readBuffer(flow_file); - const auto json_string = std::string(reinterpret_cast<const char*>(flow_file_read_result.buffer.data()), flow_file_read_result.buffer.size()); + const auto json_string = to_string(session.readBuffer(flow_file)); if (json_string.empty()) { logger_->log_error("FlowFile content is empty, transferring to Failure relationship"); session.transfer(flow_file, Failure); diff --git a/extensions/standard-processors/processors/EvaluateJsonPath.h b/extensions/standard-processors/processors/EvaluateJsonPath.h index 79af1a806..4d1d76926 100644 --- a/extensions/standard-processors/processors/EvaluateJsonPath.h +++ b/extensions/standard-processors/processors/EvaluateJsonPath.h @@ -123,8 +123,7 @@ class EvaluateJsonPath final : public core::ProcessorImpl { "value unless 'Path Not Found Behaviour' is set to 'skip', and the FlowFile will always be routed to 'matched.'"; EXTENSIONAPI static constexpr auto Destination = core::PropertyDefinitionBuilder<2>::createProperty("Destination") - .withDescription("Indicates whether the results of the JsonPath evaluation are written to the FlowFile content or a FlowFile attribute. If using attribute, must specify the Attribute Name " - "property. If set to flowfile-content, only one JsonPath may be specified, and the property name is ignored.") + .withDescription("Indicates whether the results of the JsonPath evaluation are written to the FlowFile content or a FlowFile attribute.") .withAllowedValues(magic_enum::enum_names<evaluate_json_path::DestinationType>()) .withDefaultValue(magic_enum::enum_name(evaluate_json_path::DestinationType::FlowFileAttribute)) .isRequired(true) @@ -166,6 +165,11 @@ class EvaluateJsonPath final : public core::ProcessorImpl { EXTENSIONAPI static constexpr auto Relationships = std::array{Failure, Matched, Unmatched}; EXTENSIONAPI static constexpr bool SupportsDynamicProperties = true; + EXTENSIONAPI static constexpr auto EvaluationResult = core::DynamicProperty{"Evaluation Result", "JsonPath expression to evaluate", "Dynamic property values are evaluated as JsonPaths. " + "In case of 'flowfile-conent' destination, only one dynamic property with JsonPath may be specified, in this case the name of the property is ignored. " + "In case of 'flowfile-attribute' destination, the result of the JsonPath evaluation is written to the 'Evaluation Result' property.", true}; + EXTENSIONAPI static constexpr auto DynamicProperties = std::array{EvaluationResult}; + EXTENSIONAPI static constexpr bool SupportsDynamicRelationships = false; EXTENSIONAPI static constexpr core::annotation::Input InputRequirement = core::annotation::Input::INPUT_REQUIRED; EXTENSIONAPI static constexpr bool IsSingleThreaded = false; diff --git a/extensions/standard-processors/tests/unit/EvaluateJsonPathTests.cpp b/extensions/standard-processors/tests/unit/EvaluateJsonPathTests.cpp index 12c354303..379d98fce 100644 --- a/extensions/standard-processors/tests/unit/EvaluateJsonPathTests.cpp +++ b/extensions/standard-processors/tests/unit/EvaluateJsonPathTests.cpp @@ -36,6 +36,10 @@ class EvaluateJsonPathTestFixture { processors::EvaluateJsonPath* evaluate_json_path_processor_; }; +TEST_CASE_METHOD(EvaluateJsonPathTestFixture, "At least one dynamic property must be specified", "[EvaluateJsonPathTests]") { + REQUIRE_THROWS_WITH(controller_.trigger({{.content = "foo"}}), "Process Schedule Operation: At least one dynamic property must be specified with a valid JSON path expression"); +} + TEST_CASE_METHOD(EvaluateJsonPathTestFixture, "When destination is set to flowfile content only one dynamic property is allowed", "[EvaluateJsonPathTests]") { controller_.plan->setProperty(evaluate_json_path_processor_, processors::EvaluateJsonPath::Destination, "flowfile-content"); controller_.plan->setDynamicProperty(evaluate_json_path_processor_, "attribute1", "value1"); @@ -46,6 +50,7 @@ TEST_CASE_METHOD(EvaluateJsonPathTestFixture, "When destination is set to flowfi TEST_CASE_METHOD(EvaluateJsonPathTestFixture, "Input flowfile has invalid JSON as content", "[EvaluateJsonPathTests]") { ProcessorTriggerResult result; std::string error_log; + controller_.plan->setDynamicProperty(evaluate_json_path_processor_, "attribute1", "value1"); SECTION("Flow file content is empty") { result = controller_.trigger({{.content = ""}}); error_log = "FlowFile content is empty, transferring to Failure relationship"; @@ -115,12 +120,10 @@ TEST_CASE_METHOD(EvaluateJsonPathTestFixture, "JSON paths are not found in conte CHECK(controller_.plan->getContent(result_flow_file) == "{}"); for (const auto& [key, value] : expected_attributes) { - std::string attribute_value; if (!expect_attributes) { - CHECK_FALSE(result_flow_file->getAttribute(key, attribute_value)); + CHECK_FALSE(result_flow_file->getAttribute(key)); } else { - CHECK(result_flow_file->getAttribute(key, attribute_value)); - CHECK(attribute_value == value); + CHECK(*result_flow_file->getAttribute(key) == value); } } @@ -157,9 +160,7 @@ TEST_CASE_METHOD(EvaluateJsonPathTestFixture, "JSON paths are not found in conte const auto result_flow_file = result.at(processors::EvaluateJsonPath::Unmatched).at(0); CHECK(controller_.plan->getContent(result_flow_file) == "{}"); - - std::string attribute_value; - CHECK_FALSE(result_flow_file->getAttribute("attribute", attribute_value)); + CHECK_FALSE(result_flow_file->getAttribute("attribute")); if (warn_path_not_found_behavior) { CHECK(utils::verifyLogLinePresenceInPollTime(0s, "JSON path '$.firstName' not found for attribute key 'attribute'")); @@ -173,6 +174,11 @@ TEST_CASE_METHOD(EvaluateJsonPathTestFixture, "JSON path query result does not m controller_.plan->setProperty(evaluate_json_path_processor_, processors::EvaluateJsonPath::Destination, "flowfile-attribute"); } + SECTION("Return type is set to scalar with flowfile-content destination") { + controller_.plan->setProperty(evaluate_json_path_processor_, processors::EvaluateJsonPath::ReturnType, "scalar"); + controller_.plan->setProperty(evaluate_json_path_processor_, processors::EvaluateJsonPath::Destination, "flowfile-content"); + } + std::string json_content = R"({"name": {"firstName": "John", "lastName": "Doe"}})"; auto result = controller_.trigger({{.content = json_content}}); @@ -183,8 +189,7 @@ TEST_CASE_METHOD(EvaluateJsonPathTestFixture, "JSON path query result does not m const auto result_flow_file = result.at(processors::EvaluateJsonPath::Failure).at(0); CHECK(controller_.plan->getContent(result_flow_file) == json_content); - std::string attribute_value; - CHECK_FALSE(result_flow_file->getAttribute("attribute", attribute_value)); + CHECK_FALSE(result_flow_file->getAttribute("attribute")); CHECK(utils::verifyLogLinePresenceInPollTime(0s, "JSON path '$.name' returned a non-scalar value or multiple values for attribute key 'attribute', transferring to Failure relationship")); } @@ -202,8 +207,7 @@ TEST_CASE_METHOD(EvaluateJsonPathTestFixture, "Query JSON object and write it to const auto result_flow_file = result.at(processors::EvaluateJsonPath::Matched).at(0); CHECK(controller_.plan->getContent(result_flow_file) == R"({"firstName":"John","lastName":"Doe"})"); - std::string attribute_value; - CHECK_FALSE(result_flow_file->getAttribute("jsonPath", attribute_value)); + CHECK_FALSE(result_flow_file->getAttribute("jsonPath")); } TEST_CASE_METHOD(EvaluateJsonPathTestFixture, "Query multiple scalars and write them to attributes", "[EvaluateJsonPathTests]") { @@ -222,12 +226,9 @@ TEST_CASE_METHOD(EvaluateJsonPathTestFixture, "Query multiple scalars and write const auto result_flow_file = result.at(processors::EvaluateJsonPath::Matched).at(0); CHECK(controller_.plan->getContent(result_flow_file) == json_content); - auto attribute_value = result_flow_file->getAttribute("firstName"); - CHECK(attribute_value.value() == "John"); - attribute_value = result_flow_file->getAttribute("lastName"); - CHECK(attribute_value.value() == "Doe"); - attribute_value = result_flow_file->getAttribute("id"); - CHECK(attribute_value.value() == "1234"); + CHECK(*result_flow_file->getAttribute("firstName") == "John"); + CHECK(*result_flow_file->getAttribute("lastName") == "Doe"); + CHECK(*result_flow_file->getAttribute("id") == "1234"); } TEST_CASE_METHOD(EvaluateJsonPathTestFixture, "Query a single scalar and write it to flow file", "[EvaluateJsonPathTests]") { @@ -244,8 +245,7 @@ TEST_CASE_METHOD(EvaluateJsonPathTestFixture, "Query a single scalar and write i const auto result_flow_file = result.at(processors::EvaluateJsonPath::Matched).at(0); CHECK(controller_.plan->getContent(result_flow_file) == "John"); - std::string attribute_value; - CHECK_FALSE(result_flow_file->getAttribute("firstName", attribute_value)); + CHECK_FALSE(result_flow_file->getAttribute("firstName")); } TEST_CASE_METHOD(EvaluateJsonPathTestFixture, "Query has multiple results", "[EvaluateJsonPathTests]") { @@ -262,8 +262,7 @@ TEST_CASE_METHOD(EvaluateJsonPathTestFixture, "Query has multiple results", "[Ev const auto result_flow_file = result.at(processors::EvaluateJsonPath::Matched).at(0); CHECK(controller_.plan->getContent(result_flow_file) == "[\"John\",\"Jane\"]"); - std::string attribute_value; - CHECK_FALSE(result_flow_file->getAttribute("firstName", attribute_value)); + CHECK_FALSE(result_flow_file->getAttribute("firstName")); } TEST_CASE_METHOD(EvaluateJsonPathTestFixture, "Query result is null value in flow file content", "[EvaluateJsonPathTests]") { @@ -290,8 +289,7 @@ TEST_CASE_METHOD(EvaluateJsonPathTestFixture, "Query result is null value in flo const auto result_flow_file = result.at(processors::EvaluateJsonPath::Matched).at(0); CHECK(controller_.plan->getContent(result_flow_file) == expected_content); - std::string attribute_value; - CHECK_FALSE(result_flow_file->getAttribute("firstName", attribute_value)); + CHECK_FALSE(result_flow_file->getAttribute("firstName")); } TEST_CASE_METHOD(EvaluateJsonPathTestFixture, "Query result is null value in flow file attribute", "[EvaluateJsonPathTests]") { @@ -319,12 +317,8 @@ TEST_CASE_METHOD(EvaluateJsonPathTestFixture, "Query result is null value in flo const auto result_flow_file = result.at(processors::EvaluateJsonPath::Matched).at(0); CHECK(controller_.plan->getContent(result_flow_file) == json_content); - auto attribute = result_flow_file->getAttribute("firstName"); - REQUIRE(attribute); - CHECK(attribute.value() == "John"); - attribute = result_flow_file->getAttribute("email"); - REQUIRE(attribute); - CHECK(attribute.value() == expected_null_value); + CHECK(*result_flow_file->getAttribute("firstName") == "John"); + CHECK(*result_flow_file->getAttribute("email") == expected_null_value); } } // namespace org::apache::nifi::minifi::test
