Repository: nifi-minifi-cpp Updated Branches: refs/heads/master 1ec799039 -> 45acefaac
MINIFICPP-369 Added multiple attribute/aggregate EL functions This closes #325. Signed-off-by: Aldrin Piri <[email protected]> Project: http://git-wip-us.apache.org/repos/asf/nifi-minifi-cpp/repo Commit: http://git-wip-us.apache.org/repos/asf/nifi-minifi-cpp/commit/45acefaa Tree: http://git-wip-us.apache.org/repos/asf/nifi-minifi-cpp/tree/45acefaa Diff: http://git-wip-us.apache.org/repos/asf/nifi-minifi-cpp/diff/45acefaa Branch: refs/heads/master Commit: 45acefaaca12cd0d1325918f6f3400bf2163a452 Parents: 1ec7990 Author: Andrew I. Christianson <[email protected]> Authored: Wed May 9 16:09:59 2018 -0400 Committer: Aldrin Piri <[email protected]> Committed: Fri May 18 14:17:25 2018 -0400 ---------------------------------------------------------------------- EXPRESSIONS.md | 253 +++++++++- extensions/expression-language/Expression.cpp | 487 ++++++++++++++++++- .../impl/expression/Expression.h | 49 +- .../ExpressionLanguageTests.cpp | 199 ++++++++ 4 files changed, 948 insertions(+), 40 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/nifi-minifi-cpp/blob/45acefaa/EXPRESSIONS.md ---------------------------------------------------------------------- diff --git a/EXPRESSIONS.md b/EXPRESSIONS.md index d5a34bb..1056900 100644 --- a/EXPRESSIONS.md +++ b/EXPRESSIONS.md @@ -225,6 +225,17 @@ token, filename. - [`UUID`](#uuid) - [`literal`](#literal) +### Evaluating Multiple Attributes + +- [`anyAttribute`](#anyattribute) +- [`allAttributes`](#allattributes) +- [`anyMatchingAttribute`](#anymatchingattribute) +- [`allMatchingAttributes`](#allmatchingattributes) +- [`anyDelineatedValue`](#anydelineatedvalue) +- [`allDelineatedValues`](#alldelineatedvalues) +- [`join`](#join) +- [`count`](#count) + ## Planned Features ### Searching @@ -249,17 +260,6 @@ token, filename. - `nextInt` - `getStateValue` -### Evaluating Multiple Attributes - -- `anyAttribute` -- `allAttributes` -- `anyMatchingAttribute` -- `allMatchingAttributes` -- `anyDelineatedValue` -- `allDelineatedValues` -- `join` -- `count` - ## Unsupported Features The following EL features are currently not supported, and no support is @@ -1589,3 +1589,234 @@ to evaluate additional functions against. **Examples**: `${literal(2):gt(1)}` returns true. `${literal( ${allMatchingAttributes('a.*'):count()} ):gt(3)}` returns true if there are more than 3 attributes whose names begin with the letter a. + +## Evaluating Multiple Attributes + +When it becomes necessary to evaluate the same conditions against multiple +attributes, this can be accomplished by means of the and and or functions. +However, this quickly becomes tedious, error-prone, and difficult to maintain. +For this reason, NiFi Expression Language provides several functions for +evaluating the same conditions against groups of attributes at the same time. + +### anyAttribute + +**Description**: Checks to see if any of the given attributes, match the given +condition. This function has no subject and takes one or more arguments that +are the names of attributes to which the remainder of the Expression is to be +applied. If any of the attributes specified, when evaluated against the rest of +the Expression, returns a value of `true`, then this function will return +`true`. Otherwise, this function will return `false`. + +**Subject Type**: No Subject + +**Arguments**: + +| Argument | Description | +| - | - | +| Attribute Names | One or more attribute names to evaluate | + +**Return Type**: Boolean + +**Examples**: Given that the "abc" attribute contains the value "hello world", +"xyz" contains "good bye world", and "filename" contains "file.txt" consider +the following examples: + +| Expression | Value | +| - | - | +| `${anyAttribute("abc", "xyz"):contains("bye")}` | `true` | +| `${anyAttribute("filename","xyz"):toUpper():contains("e")}` | `false` | + +### allAttributes + +**Description**: Checks to see if all of the given attributes match the given +condition. This function has no subject and takes one or more arguments that +are the names of attributes to which the remainder of the Expression is to be +applied. If all of the attributes specified, when evaluated against the rest of +the Expression, returns a value of `true`, then this function will return +`true`. Otherwise, this function will return `false`. + +**Subject Type**: No Subject + +**Arguments**: + +| Argument | Description | +| - | - | +| Attribute Names | One or more attribute names to evaluate | + +**Return Type**: Boolean + +**Examples**: Given that the "abc" attribute contains the value "hello world", +"xyz" contains "good bye world", and "filename" contains "file.txt" consider +the following examples: + +| Expression | Value | +| - | - | +| `${allAttributes("abc", "xyz"):contains("world")}` | `true` | +| `${allAttributes("abc", "filename","xyz"):toUpper():contains("e")}` | `false` | + +### anyMatchingAttribute + +**Description**: Checks to see if any of the given attributes, match the given +condition. This function has no subject and takes one or more arguments that +are Regular Expressions to match against attribute names. Any attribute whose +name matches one of the supplied Regular Expressions will be evaluated against +the rest of the Expression. If any of the attributes specified, when evaluated +against the rest of the Expression, returns a value of `true`, then this +function will return `true`. Otherwise, this function will return `false`. + +**Subject Type**: No Subject + +**Arguments**: + +| Argument | Description | +| - | - | +| Regex | One or more Regular Expressions to evaluate against attribute names | + +**Return Type**: Boolean + +**Examples**: Given that the "abc" attribute contains the value "hello world", +"xyz" contains "good bye world", and "filename" contains "file.txt" consider +the following examples: + +| Expression | Value | +| - | - | +| `${anyMatchingAttribute("[ax].*"):contains('bye')}` | `true` | +| `${anyMatchingAttribute(".*"):isNull()}` | `false` | + +### allMatchingAttributes + +**Description**: Checks to see if any of the given attributes, match the given +condition. This function has no subject and takes one or more arguments that +are Regular Expressions to match against attribute names. Any attribute whose +name matches one of the supplied Regular Expressions will be evaluated against +the rest of the Expression. If all of the attributes specified, when evaluated +against the rest of the Expression, return a value of `true`, then this +function will return `true`. Otherwise, this function will return `false`. + +**Subject Type**: No Subject + +**Arguments**: + +| Argument | Description | +| - | - | +| Regex | One or more Regular Expressions to evaluate against attribute names | + +**Return Type**: Boolean + +**Examples**: Given that the "abc" attribute contains the value "hello world", +"xyz" contains "good bye world", and "filename" contains "file.txt" consider +the following examples: + +| Expression | Value | +| - | - | +| `${allMatchingAttributes("[ax].*"):contains("world")}` | `true` | +| `${allMatchingAttributes(".*"):isNull()}` | `false` | +| `${allMatchingAttributes("f.*"):count()}` | `1` | + +### anyDelineatedValue + +**Description**: Splits a String apart according to a delimiter that is +provided, and then evaluates each of the values against the rest of the +Expression. If the Expression, when evaluated against any of the individual +values, returns `true`, this function returns `true`. Otherwise, the function +returns `false`. + +**Subject Type**: No Subject + +**Arguments**: + +| Argument | Description | +| - | - | +| Delineated Value | The value that is delineated. This is generally an embedded Expression, though it does not have to be. | +| Delimiter | The value to use to split apart the delineatedValue argument. | + +**Return Type**: Boolean + +**Examples**: Given that the "number_list" attribute contains the value +"1,2,3,4,5", and the "word_list" attribute contains the value "the,and,or,not", +consider the following examples: + +| Expression | Value | +| - | - | +| `${anyDelineatedValue("${number_list}", ","):contains("5")}` | `true` | +| `${anyDelineatedValue("this that and", ","):equals("${word_list}")}` | `false` | + +### allDelineatedValues + +**Description**: Splits a String apart according to a delimiter that is +provided, and then evaluates each of the values against the rest of the +Expression. If the Expression, when evaluated against all of the individual +values, returns `true` in each case, then this function returns `true`. +Otherwise, the function returns `false`. + +**Subject Type**: No Subject + +**Arguments**: + +| Argument | Description | +| - | - | +| Delineated Value | The value that is delineated. This is generally an embedded Expression, though it does not have to be. | +| Delimiter | The value to use to split apart the delineatedValue argument. | + +**Return Type**: Boolean + +**Examples**: Given that the "number_list" attribute contains the value +"1,2,3,4,5", and the "word_list" attribute contains the value +"those,known,or,not", consider the following examples: + +| Expression | Value | +| - | - | +| `${allDelineatedValues("${word_list}", ","):contains("o")}` | `true` | +| `${allDelineatedValues("${number_list}", ","):count()}` | `4` | +| `${allDelineatedValues("${number_list}", ","):matches("[0-9]+")}` | `true` | +| `${allDelineatedValues("${word_list}", ","):matches('e')}` | `false` | + +### join + +**Description**: Aggregate function that concatenates multiple values with the +specified delimiter. This function may be used only in conjunction with the +`allAttributes`, `allMatchingAttributes`, and `allDelineatedValues` functions. + +**Subject Type**: String + +**Arguments**: + +| Argument | Description | +| - | - | +| Delimiter | The String delimiter to use when joining values | + +**Return Type**: String + +**Examples**: Given that the "abc" attribute contains the value "hello world", +"xyz" contains "good bye world", and "filename" contains "file.txt" consider +the following examples: + +| Expression | Value | +| - | - | +| `${allMatchingAttributes("[ax].*"):substringBefore(" "):join("-")}` | `hello-good` | +| `${allAttributes("abc", "xyz"):join(" now")}` | `hello world nowgood bye world now` | + +### count + +**Description**: Aggregate function that counts the number of non-null, +non-false values returned by the `allAttributes`, `allMatchingAttributes`, and +`allDelineatedValues`. This function may be used only in conjunction with the +`allAttributes`, `allMatchingAttributes`, and `allDelineatedValues` functions. + +**Subject Type**: Any + +**Arguments**: No arguments + +**Return Type**: Number + +**Examples**: Given that the "abc" attribute contains the value "hello world", +"xyz" contains "good bye world", and "number_list" contains "1,2,3,4,5" +consider the following examples: + +| Expression | Value | +| - | - | +| `${allMatchingAttributes("[ax].*"):substringBefore(" "):count()}` | `2` | +| `${allAttributes("abc", "xyz"):contains("world"):count()}` | `1` | +| `${allDelineatedValues(${number_list}, ","):count()}` | `5` | +| `${allAttributes("abc", "non-existent-attr", "xyz"):count()}` | `2` | +| `${allMatchingAttributes(".*"):length():gt(10):count()}` | `2` | http://git-wip-us.apache.org/repos/asf/nifi-minifi-cpp/blob/45acefaa/extensions/expression-language/Expression.cpp ---------------------------------------------------------------------- diff --git a/extensions/expression-language/Expression.cpp b/extensions/expression-language/Expression.cpp index 4a93b1c..6c5b5b0 100644 --- a/extensions/expression-language/Expression.cpp +++ b/extensions/expression-language/Expression.cpp @@ -52,12 +52,13 @@ Expression make_static(std::string val) { return Expression(Value(val)); } -Expression make_dynamic(const std::function<Value(const Parameters ¶ms)> &val_fn) { +Expression make_dynamic(const std::function<Value(const Parameters ¶ms, + const std::vector<Expression> &sub_exprs)> &val_fn) { return Expression(Value(), val_fn); } Expression make_dynamic_attr(const std::string &attribute_id) { - return make_dynamic([attribute_id](const Parameters ¶ms) -> Value { + return make_dynamic([attribute_id](const Parameters ¶ms, const std::vector<Expression> &sub_exprs) -> Value { std::string result; if (params.flow_file.lock()->getAttribute(attribute_id, result)) { return Value(result); @@ -1578,17 +1579,27 @@ Expression make_dynamic_function_incomplete(const std::string &function_name, throw std::runtime_error(message_ss.str()); } - auto result = make_dynamic([=](const Parameters ¶ms) -> Value { - std::vector<Value> evaluated_args; + if (!args.empty() && args[0].is_multi()) { + std::vector<Expression> multi_args; - for (const auto &arg : args) { - evaluated_args.emplace_back(arg(params)); + for (auto it = std::next(args.begin()); it != args.end(); ++it) { + multi_args.emplace_back(*it); } - return T(evaluated_args); - }); + return args[0].compose_multi([=](const std::vector<Value> &args) -> Value { + return T(args); + }, multi_args); + } else { + return make_dynamic([=](const Parameters ¶ms, const std::vector<Expression> &sub_exprs) -> Value { + std::vector<Value> evaluated_args; - return result; + for (const auto &arg : args) { + evaluated_args.emplace_back(arg(params)); + } + + return T(evaluated_args); + }); + } } Value expr_literal(const std::vector<Value> &args) { @@ -1690,6 +1701,366 @@ Value expr_ifElse(const std::vector<Value> &args) { } } +Expression make_allAttributes(const std::string &function_name, const std::vector<Expression> &args) { + if (args.size() < 1) { + std::stringstream message_ss; + message_ss << "Expression language function " + << function_name + << " called with " + << args.size() + << " argument(s), but " + << 1 + << " are required"; + throw std::runtime_error(message_ss.str()); + } + + auto result = make_dynamic([=](const Parameters ¶ms, const std::vector<Expression> &sub_exprs) -> Value { + std::vector<Value> evaluated_args; + + bool all_true = true; + + for (const auto &sub_expr : sub_exprs) { + all_true = all_true && sub_expr(params).asBoolean(); + } + + return Value(all_true); + }); + + result.make_multi([=](const Parameters ¶ms) -> std::vector<Expression> { + std::vector<Expression> out_exprs; + + for (const auto &arg : args) { + out_exprs.emplace_back(make_dynamic([=](const Parameters ¶ms, + const std::vector<Expression> &sub_exprs) -> Value { + std::string attr_id; + attr_id = arg(params).asString(); + std::string attr_val; + + if (params.flow_file.lock()->getAttribute(attr_id, attr_val)) { + return Value(attr_val); + } else { + return Value(); + } + })); + } + + return out_exprs; + }); + + return result; +} + +Expression make_anyAttribute(const std::string &function_name, const std::vector<Expression> &args) { + if (args.size() < 1) { + std::stringstream message_ss; + message_ss << "Expression language function " + << function_name + << " called with " + << args.size() + << " argument(s), but " + << 1 + << " are required"; + throw std::runtime_error(message_ss.str()); + } + + auto result = make_dynamic([=](const Parameters ¶ms, const std::vector<Expression> &sub_exprs) -> Value { + std::vector<Value> evaluated_args; + + bool any_true = false; + + for (const auto &sub_expr : sub_exprs) { + any_true = any_true || sub_expr(params).asBoolean(); + } + + return Value(any_true); + }); + + result.make_multi([=](const Parameters ¶ms) -> std::vector<Expression> { + std::vector<Expression> out_exprs; + + for (const auto &arg : args) { + out_exprs.emplace_back(make_dynamic([=](const Parameters ¶ms, + const std::vector<Expression> &sub_exprs) -> Value { + std::string attr_id; + attr_id = arg(params).asString(); + std::string attr_val; + + if (params.flow_file.lock()->getAttribute(attr_id, attr_val)) { + return Value(attr_val); + } else { + return Value(); + } + })); + } + + return out_exprs; + }); + + return result; +} + +#ifdef EXPRESSION_LANGUAGE_USE_REGEX + +Expression make_allMatchingAttributes(const std::string &function_name, const std::vector<Expression> &args) { + if (args.size() < 1) { + std::stringstream message_ss; + message_ss << "Expression language function " + << function_name + << " called with " + << args.size() + << " argument(s), but " + << 1 + << " are required"; + throw std::runtime_error(message_ss.str()); + } + + auto result = make_dynamic([=](const Parameters ¶ms, const std::vector<Expression> &sub_exprs) -> Value { + std::vector<Value> evaluated_args; + + bool all_true = !sub_exprs.empty(); + + for (const auto &sub_expr : sub_exprs) { + all_true = all_true && sub_expr(params).asBoolean(); + } + + return Value(all_true); + }); + + result.make_multi([=](const Parameters ¶ms) -> std::vector<Expression> { + std::vector<Expression> out_exprs; + + for (const auto &arg : args) { + const std::regex attr_regex = std::regex(arg(params).asString()); + auto attrs = params.flow_file.lock()->getAttributes(); + + for (const auto &attr : attrs) { + if (std::regex_match(attr.first.begin(), attr.first.end(), attr_regex)) { + out_exprs.emplace_back(make_dynamic([=](const Parameters ¶ms, + const std::vector<Expression> &sub_exprs) -> Value { + std::string attr_val; + + if (params.flow_file.lock()->getAttribute(attr.first, attr_val)) { + return Value(attr_val); + } else { + return Value(); + } + })); + } + } + } + + return out_exprs; + }); + + return result; +} + +Expression make_anyMatchingAttribute(const std::string &function_name, const std::vector<Expression> &args) { + if (args.size() < 1) { + std::stringstream message_ss; + message_ss << "Expression language function " + << function_name + << " called with " + << args.size() + << " argument(s), but " + << 1 + << " are required"; + throw std::runtime_error(message_ss.str()); + } + + auto result = make_dynamic([=](const Parameters ¶ms, const std::vector<Expression> &sub_exprs) -> Value { + std::vector<Value> evaluated_args; + + bool any_true = false; + + for (const auto &sub_expr : sub_exprs) { + any_true = any_true || sub_expr(params).asBoolean(); + } + + return Value(any_true); + }); + + result.make_multi([=](const Parameters ¶ms) -> std::vector<Expression> { + std::vector<Expression> out_exprs; + + for (const auto &arg : args) { + const std::regex attr_regex = std::regex(arg(params).asString()); + auto attrs = params.flow_file.lock()->getAttributes(); + + for (const auto &attr : attrs) { + if (std::regex_match(attr.first.begin(), attr.first.end(), attr_regex)) { + out_exprs.emplace_back(make_dynamic([=](const Parameters ¶ms, + const std::vector<Expression> &sub_exprs) -> Value { + std::string attr_val; + + if (params.flow_file.lock()->getAttribute(attr.first, attr_val)) { + return Value(attr_val); + } else { + return Value(); + } + })); + } + } + } + + return out_exprs; + }); + + return result; +} + +#endif // EXPRESSION_LANGUAGE_USE_REGEX + +Expression make_allDelineatedValues(const std::string &function_name, const std::vector<Expression> &args) { + if (args.size() != 2) { + std::stringstream message_ss; + message_ss << "Expression language function " + << function_name + << " called with " + << args.size() + << " argument(s), but " + << 2 + << " are required"; + throw std::runtime_error(message_ss.str()); + } + + auto result = make_dynamic([=](const Parameters ¶ms, const std::vector<Expression> &sub_exprs) -> Value { + std::vector<Value> evaluated_args; + + bool all_true = !sub_exprs.empty(); + + for (const auto &sub_expr : sub_exprs) { + all_true = all_true && sub_expr(params).asBoolean(); + } + + return Value(all_true); + }); + + result.make_multi([=](const Parameters ¶ms) -> std::vector<Expression> { + std::vector<Expression> out_exprs; + + for (const auto &val : utils::StringUtils::split(args[0](params).asString(), args[1](params).asString())) { + out_exprs.emplace_back(make_static(val)); + } + + return out_exprs; + }); + + return result; +} + +Expression make_anyDelineatedValue(const std::string &function_name, const std::vector<Expression> &args) { + if (args.size() != 2) { + std::stringstream message_ss; + message_ss << "Expression language function " + << function_name + << " called with " + << args.size() + << " argument(s), but " + << 2 + << " are required"; + throw std::runtime_error(message_ss.str()); + } + + auto result = make_dynamic([=](const Parameters ¶ms, const std::vector<Expression> &sub_exprs) -> Value { + std::vector<Value> evaluated_args; + + bool any_true = false; + + for (const auto &sub_expr : sub_exprs) { + any_true = any_true || sub_expr(params).asBoolean(); + } + + return Value(any_true); + }); + + result.make_multi([=](const Parameters ¶ms) -> std::vector<Expression> { + std::vector<Expression> out_exprs; + + for (const auto &val : utils::StringUtils::split(args[0](params).asString(), args[1](params).asString())) { + out_exprs.emplace_back(make_static(val)); + } + + return out_exprs; + }); + + return result; +} + +Expression make_count(const std::string &function_name, const std::vector<Expression> &args) { + if (args.size() != 1) { + std::stringstream message_ss; + message_ss << "Expression language function " + << function_name + << " called with " + << args.size() + << " argument(s), but " + << 1 + << " are required"; + throw std::runtime_error(message_ss.str()); + } + + if (!args[0].is_multi()) { + std::stringstream message_ss; + message_ss << "Expression language function " + << function_name + << " called against singular expression."; + throw std::runtime_error(message_ss.str()); + } + + return args[0].make_aggregate([](const Parameters ¶ms, + const std::vector<Expression> &sub_exprs) -> Value { + uint64_t count = 0; + for (const auto &sub_expr : sub_exprs) { + if (sub_expr(params).asBoolean()) { + count++; + } + } + return Value(count); + }); +} + +Expression make_join(const std::string &function_name, const std::vector<Expression> &args) { + if (args.size() != 2) { + std::stringstream message_ss; + message_ss << "Expression language function " + << function_name + << " called with " + << args.size() + << " argument(s), but " + << 2 + << " are required"; + throw std::runtime_error(message_ss.str()); + } + + if (!args[0].is_multi()) { + std::stringstream message_ss; + message_ss << "Expression language function " + << function_name + << " called against singular expression."; + throw std::runtime_error(message_ss.str()); + } + + auto delim_expr = args[1]; + + return args[0].make_aggregate([delim_expr](const Parameters ¶ms, + const std::vector<Expression> &sub_exprs) -> Value { + std::string delim = delim_expr(params).asString(); + std::stringstream out_ss; + bool first = true; + + for (const auto &sub_expr : sub_exprs) { + if (!first) { + out_ss << delim; + } + out_ss << sub_expr(params).asString(); + first = false; + } + + return Value(out_ss.str()); + }); +} + Expression make_dynamic_function(const std::string &function_name, const std::vector<Expression> &args) { if (function_name == "hostname") { @@ -1769,6 +2140,10 @@ Expression make_dynamic_function(const std::string &function_name, return make_dynamic_function_incomplete<expr_matches>(function_name, args, 1); } else if (function_name == "find") { return make_dynamic_function_incomplete<expr_find>(function_name, args, 1); + } else if (function_name == "allMatchingAttributes") { + return make_allMatchingAttributes(function_name, args); + } else if (function_name == "anyMatchingAttribute") { + return make_anyMatchingAttribute(function_name, args); #endif // EXPRESSION_LANGUAGE_USE_REGEX } else if (function_name == "trim") { return make_dynamic_function_incomplete<expr_trim>(function_name, args, 0); @@ -1822,6 +2197,18 @@ Expression make_dynamic_function(const std::string &function_name, return make_dynamic_function_incomplete<expr_not>(function_name, args, 0); } else if (function_name == "ifElse") { return make_dynamic_function_incomplete<expr_ifElse>(function_name, args, 2); + } else if (function_name == "allAttributes") { + return make_allAttributes(function_name, args); + } else if (function_name == "anyAttribute") { + return make_anyAttribute(function_name, args); + } else if (function_name == "allDelineatedValues") { + return make_allDelineatedValues(function_name, args); + } else if (function_name == "anyDelineatedValue") { + return make_anyDelineatedValue(function_name, args); + } else if (function_name == "count") { + return make_count(function_name, args); + } else if (function_name == "join") { + return make_join(function_name, args); } else { std::string msg("Unknown expression function: "); msg.append(function_name); @@ -1843,7 +2230,7 @@ Expression make_function_composition(const Expression &arg, return expr; } -bool Expression::isDynamic() const { +bool Expression::is_dynamic() const { if (val_fn_) { return true; } else { @@ -1852,28 +2239,42 @@ bool Expression::isDynamic() const { } Expression Expression::operator+(const Expression &other_expr) const { - if (isDynamic() && other_expr.isDynamic()) { + if (is_dynamic() && other_expr.is_dynamic()) { auto val_fn = val_fn_; auto other_val_fn = other_expr.val_fn_; - return make_dynamic([val_fn, other_val_fn](const Parameters ¶ms) -> Value { - Value result = val_fn(params); - return Value(result.asString().append(other_val_fn(params).asString())); + auto sub_expr_generator = sub_expr_generator_; + auto other_sub_expr_generator = other_expr.sub_expr_generator_; + return make_dynamic([val_fn, + other_val_fn, + sub_expr_generator, + other_sub_expr_generator](const Parameters ¶ms, + const std::vector<Expression> &sub_exprs) -> Value { + Value result = val_fn(params, sub_expr_generator(params)); + return Value(result.asString().append(other_val_fn(params, other_sub_expr_generator(params)).asString())); }); - } else if (isDynamic() && !other_expr.isDynamic()) { + } else if (is_dynamic() && !other_expr.is_dynamic()) { auto val_fn = val_fn_; auto other_val = other_expr.val_; - return make_dynamic([val_fn, other_val](const Parameters ¶ms) -> Value { - Value result = val_fn(params); + auto sub_expr_generator = sub_expr_generator_; + return make_dynamic([val_fn, + other_val, + sub_expr_generator](const Parameters ¶ms, + const std::vector<Expression> &sub_exprs) -> Value { + Value result = val_fn(params, sub_expr_generator(params)); return Value(result.asString().append(other_val.asString())); }); - } else if (!isDynamic() && other_expr.isDynamic()) { + } else if (!is_dynamic() && other_expr.is_dynamic()) { auto val = val_; auto other_val_fn = other_expr.val_fn_; - return make_dynamic([val, other_val_fn](const Parameters ¶ms) -> Value { + auto other_sub_expr_generator = other_expr.sub_expr_generator_; + return make_dynamic([val, + other_val_fn, + other_sub_expr_generator](const Parameters ¶ms, + const std::vector<Expression> &sub_exprs) -> Value { Value result(val); - return Value(result.asString().append(other_val_fn(params).asString())); + return Value(result.asString().append(other_val_fn(params, other_sub_expr_generator(params)).asString())); }); - } else if (!isDynamic() && !other_expr.isDynamic()) { + } else if (!is_dynamic() && !other_expr.is_dynamic()) { std::string result(val_.asString()); result.append(other_expr.val_.asString()); return make_static(result); @@ -1883,13 +2284,53 @@ Expression Expression::operator+(const Expression &other_expr) const { } Value Expression::operator()(const Parameters ¶ms) const { - if (isDynamic()) { - return val_fn_(params); + if (is_dynamic()) { + return val_fn_(params, sub_expr_generator_(params)); } else { return val_; } } +Expression Expression::compose_multi(const std::function<Value(const std::vector<Value> &)> fn, + const std::vector<Expression> &args) const { + auto result = make_dynamic(val_fn_); + auto compose_expr_generator = sub_expr_generator_; + + result.sub_expr_generator_ = [=](const Parameters ¶ms) -> std::vector<Expression> { + auto sub_exprs = compose_expr_generator(params); + std::vector<Expression> out_exprs{}; + for (const auto &sub_expr : sub_exprs) { + out_exprs.emplace_back(make_dynamic([=](const Parameters ¶ms, + const std::vector<Expression> &sub_exprs) { + std::vector<Value> evaluated_args; + + evaluated_args.emplace_back(sub_expr(params)); + + for (const auto &arg : args) { + evaluated_args.emplace_back(arg(params)); + } + + return fn(evaluated_args); + })); + } + return out_exprs; + }; + + result.is_multi_ = true; + + return result; +} + +Expression Expression::make_aggregate(std::function<Value(const Parameters ¶ms, + const std::vector<Expression> &sub_exprs)> val_fn) const { + auto sub_expr_generator = sub_expr_generator_; + return make_dynamic([sub_expr_generator, + val_fn](const Parameters ¶ms, + const std::vector<Expression> &sub_exprs) -> Value { + return val_fn(params, sub_expr_generator(params)); + }); +} + } /* namespace expression */ } /* namespace minifi */ } /* namespace nifi */ http://git-wip-us.apache.org/repos/asf/nifi-minifi-cpp/blob/45acefaa/extensions/expression-language/impl/expression/Expression.h ---------------------------------------------------------------------- diff --git a/extensions/expression-language/impl/expression/Expression.h b/extensions/expression-language/impl/expression/Expression.h index f123f77..b7bf676 100644 --- a/extensions/expression-language/impl/expression/Expression.h +++ b/extensions/expression-language/impl/expression/Expression.h @@ -42,7 +42,9 @@ typedef struct { std::weak_ptr<core::FlowFile> flow_file; } Parameters; -static const std::function<Value(const Parameters ¶ms)> NOOP_FN; +class Expression; + +static const std::function<Value(const Parameters ¶ms, const std::vector<Expression> &sub_exprs)> NOOP_FN; /** * A NiFi expression language expression. @@ -55,10 +57,12 @@ class Expression { } explicit Expression(Value val, - std::function<Value(const Parameters &)> val_fn = NOOP_FN) + std::function<Value(const Parameters &, const std::vector<Expression> &)> val_fn = NOOP_FN) : val_fn_(std::move(val_fn)), - fn_args_() { + fn_args_(), + is_multi_(false) { val_ = val; + sub_expr_generator_ = [](const Parameters ¶ms) -> std::vector<Expression> { return {}; }; } /** @@ -68,7 +72,14 @@ class Expression { * * @return true if expression is dynamic */ - bool isDynamic() const; + bool is_dynamic() const; + + /** + * Whether or not this expression is a multi-expression. + */ + bool is_multi() const { + return is_multi_; + } /** * Combine this expression with another expression by concatenation. Intermediate results @@ -87,10 +98,35 @@ class Expression { */ Value operator()(const Parameters ¶ms) const; + /** + * Turn this expression into a multi-expression which generates subexpressions dynamically. + * + * @param sub_expr_generator function which generates expressions according to given params + */ + void make_multi(std::function<std::vector<Expression>(const Parameters ¶ms)> sub_expr_generator) { + this->sub_expr_generator_ = std::move(sub_expr_generator); + is_multi_ = true; + } + + /** + * Composes a multi-expression statically with the given fn. + * + * @param fn Value function to compose with + * @param args function arguments + * @return composed multi-expression + */ + Expression compose_multi(const std::function<Value(const std::vector<Value> &)> fn, + const std::vector<Expression> &args) const; + + Expression make_aggregate(std::function<Value(const Parameters ¶ms, + const std::vector<Expression> &sub_exprs)> val_fn) const; + protected: Value val_; - std::function<Value(const Parameters ¶ms)> val_fn_; + std::function<Value(const Parameters ¶ms, const std::vector<Expression> &sub_exprs)> val_fn_; std::vector<Expression> fn_args_; + std::function<std::vector<Expression>(const Parameters ¶ms)> sub_expr_generator_; + bool is_multi_; }; /** @@ -115,7 +151,8 @@ Expression make_static(std::string val); * @param val_fn * @return */ -Expression make_dynamic(const std::function<Value(const Parameters ¶ms)> &val_fn); +Expression make_dynamic(const std::function<Value(const Parameters ¶ms, + const std::vector<Expression> &sub_exprs)> &val_fn); /** * Creates a dynamic expression which evaluates the given flow file attribute. http://git-wip-us.apache.org/repos/asf/nifi-minifi-cpp/blob/45acefaa/libminifi/test/expression-language-tests/ExpressionLanguageTests.cpp ---------------------------------------------------------------------- diff --git a/libminifi/test/expression-language-tests/ExpressionLanguageTests.cpp b/libminifi/test/expression-language-tests/ExpressionLanguageTests.cpp index 7d98644..232deb4 100644 --- a/libminifi/test/expression-language-tests/ExpressionLanguageTests.cpp +++ b/libminifi/test/expression-language-tests/ExpressionLanguageTests.cpp @@ -1285,3 +1285,202 @@ TEST_CASE("Encode Decode B64", "[expressionEncodeDecodeB64]") { // NOLINT flow_file_a->addAttribute("message", "Zero > One < \"two!\" & 'true'"); REQUIRE("Zero > One < \"two!\" & 'true'" == expr({flow_file_a}).asString()); } + +TEST_CASE("All Contains", "[expressionAllContains]") { // NOLINT + auto expr = expression::compile("${allAttributes('a', 'b'):contains('hello')}"); + + auto flow_file_a = std::make_shared<MockFlowFile>(); + flow_file_a->addAttribute("a", "hello 1"); + flow_file_a->addAttribute("b", "hello 2"); + REQUIRE(expr({flow_file_a}).asBoolean()); +} + +TEST_CASE("All Contains 2", "[expressionAllContains2]") { // NOLINT + auto expr = expression::compile("${allAttributes('a', 'b'):contains('hello')}"); + + auto flow_file_a = std::make_shared<MockFlowFile>(); + flow_file_a->addAttribute("a", "hello 1"); + flow_file_a->addAttribute("b", "mello 2"); + REQUIRE(!expr({flow_file_a}).asBoolean()); +} + +TEST_CASE("Any Contains", "[expressionAnyContains]") { // NOLINT + auto expr = expression::compile("${anyAttribute('a', 'b'):contains('hello')}"); + + auto flow_file_a = std::make_shared<MockFlowFile>(); + flow_file_a->addAttribute("a", "hello 1"); + flow_file_a->addAttribute("b", "mello 2"); + REQUIRE(expr({flow_file_a}).asBoolean()); +} + +TEST_CASE("Any Contains 2", "[expressionAnyContains2]") { // NOLINT + auto expr = expression::compile("${anyAttribute('a', 'b'):contains('hello')}"); + + auto flow_file_a = std::make_shared<MockFlowFile>(); + flow_file_a->addAttribute("a", "mello 1"); + flow_file_a->addAttribute("b", "mello 2"); + REQUIRE(!expr({flow_file_a}).asBoolean()); +} + +#ifdef EXPRESSION_LANGUAGE_USE_REGEX + +TEST_CASE("All Matching Contains", "[expressionAllMatchingContains]") { // NOLINT + auto expr = expression::compile("${allMatchingAttributes('xyz_.*'):contains('hello')}"); + + auto flow_file_a = std::make_shared<MockFlowFile>(); + flow_file_a->addAttribute("xyz_1", "hello 1"); + flow_file_a->addAttribute("xyz_2", "hello 2"); + REQUIRE(expr({flow_file_a}).asBoolean()); +} + +TEST_CASE("All Matching Contains 2", "[expressionAllMatchingContains2]") { // NOLINT + auto expr = expression::compile("${allMatchingAttributes('abc_.*'):contains('hello')}"); + + auto flow_file_a = std::make_shared<MockFlowFile>(); + flow_file_a->addAttribute("xyz_1", "hello 1"); + flow_file_a->addAttribute("xyz_2", "hello 2"); + REQUIRE(!expr({flow_file_a}).asBoolean()); +} + +TEST_CASE("All Matching Contains 3", "[expressionAllMatchingContains3]") { // NOLINT + auto expr = expression::compile("${allMatchingAttributes('abc_.*'):contains('hello')}"); + + auto flow_file_a = std::make_shared<MockFlowFile>(); + flow_file_a->addAttribute("xyz_1", "hello 1"); + flow_file_a->addAttribute("abc_2", "hello 2"); + REQUIRE(expr({flow_file_a}).asBoolean()); +} + +TEST_CASE("All Matching Contains 4", "[expressionAllMatchingContains4]") { // NOLINT + auto expr = expression::compile("${allMatchingAttributes('xyz_.*'):contains('hello')}"); + + auto flow_file_a = std::make_shared<MockFlowFile>(); + flow_file_a->addAttribute("xyz_1", "hello 1"); + flow_file_a->addAttribute("xyz_2", "2"); + REQUIRE(!expr({flow_file_a}).asBoolean()); +} + +TEST_CASE("Any Matching Contains", "[expressionAnyMatchingContains]") { // NOLINT + auto expr = expression::compile("${anyMatchingAttribute('xyz_.*'):contains('hello')}"); + + auto flow_file_a = std::make_shared<MockFlowFile>(); + flow_file_a->addAttribute("xyz_1", "hello 1"); + flow_file_a->addAttribute("xyz_2", "mello 2"); + REQUIRE(expr({flow_file_a}).asBoolean()); +} + +TEST_CASE("Any Matching Contains 2", "[expressionAnyMatchingContains2]") { // NOLINT + auto expr = expression::compile("${anyMatchingAttribute('abc_.*'):contains('hello')}"); + + auto flow_file_a = std::make_shared<MockFlowFile>(); + flow_file_a->addAttribute("xyz_1", "hello 1"); + flow_file_a->addAttribute("xyz_2", "mello 2"); + REQUIRE(!expr({flow_file_a}).asBoolean()); +} + +TEST_CASE("Any Matching Contains 3", "[expressionAnyMatchingContains3]") { // NOLINT + auto expr = expression::compile("${anyMatchingAttribute('abc_.*'):contains('hello')}"); + + auto flow_file_a = std::make_shared<MockFlowFile>(); + flow_file_a->addAttribute("abc_1", "hello 1"); + flow_file_a->addAttribute("xyz_2", "mello 2"); + REQUIRE(expr({flow_file_a}).asBoolean()); +} + +TEST_CASE("Any Matching Contains 4", "[expressionAnyMatchingContains4]") { // NOLINT + auto expr = expression::compile("${anyMatchingAttribute('abc_.*'):contains('hello')}"); + + auto flow_file_a = std::make_shared<MockFlowFile>(); + flow_file_a->addAttribute("xyz_1", "mello 1"); + flow_file_a->addAttribute("xyz_2", "mello 2"); + REQUIRE(!expr({flow_file_a}).asBoolean()); +} + +#endif // EXPRESSION_LANGUAGE_USE_REGEX + +TEST_CASE("All Delineated Contains", "[expressionAllDelineatedContains]") { // NOLINT + auto expr = expression::compile("${allDelineatedValues(${word_list}, \",\"):contains('hello')}"); + + auto flow_file_a = std::make_shared<MockFlowFile>(); + flow_file_a->addAttribute("word_list", "hello_1,hello_2"); + REQUIRE(expr({flow_file_a}).asBoolean()); +} + +TEST_CASE("All Delineated Contains 2", "[expressionAllDelineatedContains2]") { // NOLINT + auto expr = expression::compile("${allDelineatedValues(${word_list}, \",\"):contains('hello')}"); + + auto flow_file_a = std::make_shared<MockFlowFile>(); + flow_file_a->addAttribute("word_list", "hello_1,mello_2"); + REQUIRE(!expr({flow_file_a}).asBoolean()); +} + +TEST_CASE("All Delineated Contains 3", "[expressionAllDelineatedContains3]") { // NOLINT + auto expr = expression::compile("${allDelineatedValues(${word_list}, \" \"):contains('1,h')}"); + + auto flow_file_a = std::make_shared<MockFlowFile>(); + flow_file_a->addAttribute("word_list", "hello_1,hello_2"); + REQUIRE(expr({flow_file_a}).asBoolean()); +} + +TEST_CASE("Any Delineated Contains", "[expressionAnyDelineatedContains]") { // NOLINT + auto expr = expression::compile("${anyDelineatedValue(${word_list}, \",\"):contains('hello')}"); + + auto flow_file_a = std::make_shared<MockFlowFile>(); + flow_file_a->addAttribute("word_list", "hello_1,mello_2"); + REQUIRE(expr({flow_file_a}).asBoolean()); +} + +TEST_CASE("Any Delineated Contains 2", "[expressionAnyDelineatedContains2]") { // NOLINT + auto expr = expression::compile("${anyDelineatedValue(${word_list}, \",\"):contains('hello')}"); + + auto flow_file_a = std::make_shared<MockFlowFile>(); + flow_file_a->addAttribute("word_list", "mello_1,mello_2"); + REQUIRE(!expr({flow_file_a}).asBoolean()); +} + +TEST_CASE("Count", "[expressionCount]") { // NOLINT + auto expr = expression::compile("${allAttributes('a', 'b'):contains('hello'):count()}"); + + auto flow_file_a = std::make_shared<MockFlowFile>(); + flow_file_a->addAttribute("a", "hello 1"); + flow_file_a->addAttribute("b", "mello 2"); + REQUIRE(1 == expr({flow_file_a}).asUnsignedLong()); +} + +TEST_CASE("Count 2", "[expressionCount2]") { // NOLINT + auto expr = expression::compile("${allAttributes('a', 'b'):contains('mello'):count()}"); + + auto flow_file_a = std::make_shared<MockFlowFile>(); + flow_file_a->addAttribute("a", "mello 1"); + flow_file_a->addAttribute("b", "mello 2"); + flow_file_a->addAttribute("c", "hello 3"); + REQUIRE(2 == expr({flow_file_a}).asUnsignedLong()); +} + +TEST_CASE("Count 3", "[expressionCount3]") { // NOLINT + auto expr = expression::compile("abc${allAttributes('a', 'b'):contains('mello'):count()}xyz"); + + auto flow_file_a = std::make_shared<MockFlowFile>(); + flow_file_a->addAttribute("a", "mello 1"); + flow_file_a->addAttribute("b", "mello 2"); + flow_file_a->addAttribute("c", "hello 3"); + REQUIRE("abc2xyz" == expr({flow_file_a}).asString()); +} + +TEST_CASE("Join", "[expressionJoin]") { // NOLINT + auto expr = expression::compile("abc_${allAttributes('a', 'b'):prepend('def_'):append('_ghi'):join(\"|\")}_xyz"); + + auto flow_file_a = std::make_shared<MockFlowFile>(); + flow_file_a->addAttribute("a", "hello"); + flow_file_a->addAttribute("b", "mello"); + REQUIRE("abc_def_hello_ghi|def_mello_ghi_xyz" == expr({flow_file_a}).asString()); +} + +TEST_CASE("Join 2", "[expressionJoin2]") { // NOLINT + auto expr = expression::compile("abc_${allAttributes('a', 'b'):join(\"|\"):prepend('def_'):append('_ghi')}_xyz"); + + auto flow_file_a = std::make_shared<MockFlowFile>(); + flow_file_a->addAttribute("a", "hello"); + flow_file_a->addAttribute("b", "mello"); + REQUIRE("abc_def_hello|mello_ghi_xyz" == expr({flow_file_a}).asString()); +}
