diff --git a/lib/cfg-grammar.y b/lib/cfg-grammar.y index d29bb858d5..0e44d860f0 100644 --- a/lib/cfg-grammar.y +++ b/lib/cfg-grammar.y @@ -207,6 +207,7 @@ main_location_print (FILE *yyo, YYLTYPE const * const yylocp) /* operators in the filter language, the order of this determines precedence */ %right KW_ASSIGN 9000 %right '?' ':' +%right KW_NULL_COALESCING %left KW_OR 9010 %left KW_AND 9020 %left KW_STR_EQ 9030 KW_STR_NE 9031, KW_TA_EQ 9032, KW_TA_NE 9033, KW_TAV_EQ 9034, KW_TAV_NE 9035 diff --git a/lib/cfg-lex.l b/lib/cfg-lex.l index 09f9ef628a..898b32282f 100644 --- a/lib/cfg-lex.l +++ b/lib/cfg-lex.l @@ -319,6 +319,7 @@ filterx_word [^ \#'"/\(\)\{\}\[\]\\;\r\n\t,|\.@:] !== { return KW_TAV_NE; } \+= { return KW_PLUS_ASSIGN; } =~ { return KW_REGEXP_MATCH; } +\?\? { return KW_NULL_COALESCING; } (-|\+)?{digit}+\.{digit}+ { yylval->fnum = strtod(yytext, NULL); return LL_FLOAT; } 0x{xdigit}+ { diff --git a/lib/filterx/CMakeLists.txt b/lib/filterx/CMakeLists.txt index d236bec357..7c3c774eca 100644 --- a/lib/filterx/CMakeLists.txt +++ b/lib/filterx/CMakeLists.txt @@ -41,6 +41,7 @@ set(FILTERX_HEADERS filterx/func-vars.h filterx/func-unset-empties.h filterx/expr-plus.h + filterx/expr-null-coalesce.h PARENT_SCOPE ) @@ -88,6 +89,7 @@ set(FILTERX_SOURCES filterx/func-unset-empties.c filterx/expr-plus.c filterx/filterx-private.c + filterx/expr-null-coalesce.c PARENT_SCOPE ) diff --git a/lib/filterx/Makefile.am b/lib/filterx/Makefile.am index d0cc66ed4c..e66ed93468 100644 --- a/lib/filterx/Makefile.am +++ b/lib/filterx/Makefile.am @@ -42,7 +42,8 @@ filterxinclude_HEADERS = \ lib/filterx/func-len.h \ lib/filterx/func-vars.h \ lib/filterx/func-unset-empties.h \ - lib/filterx/filterx-private.h + lib/filterx/filterx-private.h \ + lib/filterx/expr-null-coalesce.h filterx_sources = \ @@ -89,6 +90,7 @@ filterx_sources = \ lib/filterx/func-vars.c \ lib/filterx/func-unset-empties.c \ lib/filterx/filterx-private.c \ + lib/filterx/expr-null-coalesce.c \ lib/filterx/filterx-grammar.y BUILT_SOURCES += \ diff --git a/lib/filterx/expr-null-coalesce.c b/lib/filterx/expr-null-coalesce.c new file mode 100644 index 0000000000..bdbe3c2e73 --- /dev/null +++ b/lib/filterx/expr-null-coalesce.c @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2024 Axoflow + * Copyright (c) 2024 shifter + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + * As an additional exemption you are allowed to compile & link against the + * OpenSSL libraries as published by the OpenSSL project. See the file + * COPYING for details. + * + */ + +#include "filterx/expr-null-coalesce.h" +#include "filterx/object-null.h" +#include "filterx/filterx-eval.h" +#include "filterx/object-message-value.h" + +typedef struct _FilterXNullCoalesce FilterXNullCoalesce; + +struct _FilterXNullCoalesce +{ + FilterXBinaryOp super; +}; + +static FilterXObject * +_eval(FilterXExpr *s) +{ + FilterXNullCoalesce *self = (FilterXNullCoalesce *) s; + + FilterXObject *lhs_object = filterx_expr_eval(self->super.lhs); + if (!lhs_object || filterx_object_is_type(lhs_object, &FILTERX_TYPE_NAME(null)) + || (filterx_object_is_type(lhs_object, &FILTERX_TYPE_NAME(message_value)) + && filterx_message_value_get_type(lhs_object) == LM_VT_NULL)) + { + if (!lhs_object) + { + msg_debug("FILTERX null coalesce supressing error:", + filterx_format_last_error()); + filterx_eval_clear_errors(); + } + FilterXObject *rhs_object = filterx_expr_eval(self->super.rhs); + filterx_object_unref(lhs_object); + return rhs_object; + } + return lhs_object; +} + +FilterXExpr * +filterx_null_coalesce_new(FilterXExpr *lhs, FilterXExpr *rhs) +{ + FilterXNullCoalesce *self = g_new0(FilterXNullCoalesce, 1); + filterx_binary_op_init_instance(&self->super, lhs, rhs); + self->super.super.eval = _eval; + return &self->super.super; +} diff --git a/lib/filterx/expr-null-coalesce.h b/lib/filterx/expr-null-coalesce.h new file mode 100644 index 0000000000..76cf6dda96 --- /dev/null +++ b/lib/filterx/expr-null-coalesce.h @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2024 Axoflow + * Copyright (c) 2024 shifter + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + * As an additional exemption you are allowed to compile & link against the + * OpenSSL libraries as published by the OpenSSL project. See the file + * COPYING for details. + * + */ + +#ifndef FILTERX_NULL_COALESCE_H_INCLUDED +#define FILTERX_NULL_COALESCE_H_INCLUDED + +#include "filterx/filterx-expr.h" + +FilterXExpr *filterx_null_coalesce_new(FilterXExpr *lhs, FilterXExpr *rhs); + +#endif diff --git a/lib/filterx/filterx-eval.h b/lib/filterx/filterx-eval.h index 87054509cb..143de452d0 100644 --- a/lib/filterx/filterx-eval.h +++ b/lib/filterx/filterx-eval.h @@ -56,6 +56,7 @@ void filterx_eval_set_context(FilterXEvalContext *context); gboolean filterx_eval_exec_statements(FilterXEvalContext *context, GList *statements, LogMessage *msg); void filterx_eval_sync_scope_and_message(FilterXScope *scope, LogMessage *msg); const gchar *filterx_eval_get_last_error(void); +EVTTAG *filterx_format_last_error(void); void filterx_eval_clear_errors(void); void filterx_eval_store_weak_ref(FilterXObject *object); diff --git a/lib/filterx/filterx-expr.c b/lib/filterx/filterx-expr.c index 735d3d4f9d..4e6430d905 100644 --- a/lib/filterx/filterx-expr.c +++ b/lib/filterx/filterx-expr.c @@ -124,6 +124,8 @@ filterx_binary_op_init_instance(FilterXBinaryOp *self, FilterXExpr *lhs, FilterX { filterx_expr_init_instance(&self->super); self->super.free_fn = filterx_binary_op_free_method; + g_assert(lhs); + g_assert(rhs); self->lhs = lhs; self->rhs = rhs; } diff --git a/lib/filterx/filterx-grammar.ym b/lib/filterx/filterx-grammar.ym index fd21bb1402..2fb91ac2b2 100644 --- a/lib/filterx/filterx-grammar.ym +++ b/lib/filterx/filterx-grammar.ym @@ -53,6 +53,7 @@ #include "filterx/expr-shorthand.h" #include "filterx/expr-regexp.h" #include "filterx/expr-plus.h" +#include "filterx/expr-null-coalesce.h" #include "template/templates.h" @@ -141,6 +142,7 @@ construct_template_expr(LogTemplate *template) %type if %type codeblock %type ternary +%type default %% @@ -316,6 +318,7 @@ expr | expr '+' expr { $$ = filterx_operator_plus_new($1, $3); } | '(' expr ')' { $$ = $2; } | ternary { $$ = $1; } + | default { $$ = $1; } | KW_ISSET '(' expr ')' { $$ = filterx_isset_new($3); } | KW_UNSET '(' expr ')' { $$ = filterx_unset_new($3); } | regexp_match { $$ = $1; } @@ -493,6 +496,13 @@ ternary } ; +default + : expr KW_NULL_COALESCING expr + { + $$ = filterx_null_coalesce_new($1, $3); + } + ; + /* INCLUDE_RULES */ %% diff --git a/lib/filterx/tests/CMakeLists.txt b/lib/filterx/tests/CMakeLists.txt index bfc2e014fe..85ac443f1a 100644 --- a/lib/filterx/tests/CMakeLists.txt +++ b/lib/filterx/tests/CMakeLists.txt @@ -18,4 +18,4 @@ add_unit_test(LIBTEST CRITERION TARGET test_func_istype DEPENDS json-plugin ${JS add_unit_test(LIBTEST CRITERION TARGET test_func_unset_empties DEPENDS json-plugin ${JSONC_LIBRARY}) add_unit_test(LIBTEST CRITERION TARGET test_expr_function DEPENDS json-plugin ${JSONC_LIBRARY}) add_unit_test(LIBTEST CRITERION TARGET test_expr_regexp DEPENDS json-plugin ${JSONC_LIBRARY}) - +add_unit_test(LIBTEST CRITERION TARGET test_expr_null_coalesce DEPENDS json-plugin ${JSONC_LIBRARY}) diff --git a/lib/filterx/tests/Makefile.am b/lib/filterx/tests/Makefile.am index f477bd58d3..4fa7c60de3 100644 --- a/lib/filterx/tests/Makefile.am +++ b/lib/filterx/tests/Makefile.am @@ -19,7 +19,8 @@ lib_filterx_tests_TESTS = \ lib/filterx/tests/test_type_registry \ lib/filterx/tests/test_func_istype \ lib/filterx/tests/test_func_unset_empties \ - lib/filterx/tests/test_expr_regexp + lib/filterx/tests/test_expr_regexp \ + lib/filterx/tests/test_expr_null_coalesce EXTRA_DIST += lib/filterx/tests/CMakeLists.txt @@ -84,3 +85,6 @@ lib_filterx_tests_test_expr_function_LDADD = $(TEST_LDADD) $(JSON_LIBS) lib_filterx_tests_test_expr_regexp_CFLAGS = $(TEST_CFLAGS) lib_filterx_tests_test_expr_regexp_LDADD = $(TEST_LDADD) $(JSON_LIBS) + +lib_filterx_tests_test_expr_null_coalesce_CFLAGS = $(TEST_CFLAGS) +lib_filterx_tests_test_expr_null_coalesce_LDADD = $(TEST_LDADD) $(JSON_LIBS) diff --git a/lib/filterx/tests/test_expr_null_coalesce.c b/lib/filterx/tests/test_expr_null_coalesce.c new file mode 100644 index 0000000000..342889e770 --- /dev/null +++ b/lib/filterx/tests/test_expr_null_coalesce.c @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2024 Axoflow + * Copyright (c) 2024 shifter + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + * As an additional exemption you are allowed to compile & link against the + * OpenSSL libraries as published by the OpenSSL project. See the file + * COPYING for details. + * + */ + +#include +#include "libtest/cr_template.h" +#include "libtest/filterx-lib.h" + +#include "filterx/filterx-object.h" +#include "filterx/object-primitive.h" +#include "filterx/expr-comparison.h" +#include "filterx/filterx-expr.h" +#include "filterx/expr-literal.h" +#include "filterx/object-string.h" +#include "filterx/object-null.h" +#include "filterx/object-datetime.h" +#include "filterx/object-message-value.h" +#include "filterx/expr-null-coalesce.h" +#include "filterx/func-istype.h" +#include "filterx/filterx-eval.h" +#include "filterx/func-len.h" +#include "filterx/expr-function.h" + +#include "apphook.h" +#include "scratch-buffers.h" + +Test(expr_null_coalesce, test_coalescing_soft_null) +{ + FilterXExpr *coalesce = filterx_null_coalesce_new(filterx_literal_new(filterx_null_new()), + filterx_non_literal_new(filterx_test_unknown_object_new())); + cr_assert(coalesce); + FilterXObject *res = filterx_expr_eval(coalesce); + cr_assert(res); + cr_assert(filterx_object_is_type(res, &FILTERX_TYPE_NAME(test_unknown_object))); + filterx_expr_unref(coalesce); + filterx_object_unref(res); +} + +Test(expr_null_coalesce, test_coalescing_supressing_lhs_eval_error) +{ + FilterXExpr *err_expr = filterx_dummy_error_new("lhs error"); + cr_assert_not_null(err_expr); + + // passing errorous expression as lhs + FilterXExpr *coalesce = filterx_null_coalesce_new(err_expr, filterx_non_literal_new(filterx_test_unknown_object_new())); + cr_assert(coalesce); + + // eval returns rhs value, since lhs fails during eval + FilterXObject *res = filterx_expr_eval(coalesce); + cr_assert(res); + cr_assert(filterx_object_is_type(res, &FILTERX_TYPE_NAME(test_unknown_object))); + + // lhs expr eval errors must supressed by null_coalesce + const gchar *last_error = filterx_eval_get_last_error(); + cr_assert_null(last_error); + + filterx_expr_unref(coalesce); + filterx_object_unref(res); +} + +Test(expr_null_coalesce, test_coalescing_keep_rhs_eval_error) +{ + const gchar *error_msg = "rhs error"; + FilterXExpr *err_expr = filterx_dummy_error_new(error_msg); + + // passing errorous expression as rhs + FilterXExpr *coalesce = filterx_null_coalesce_new(filterx_non_literal_new(filterx_null_new()), + err_expr); + cr_assert(coalesce); + + // null_coalesce returns null, since lhs is null and rhs fails + FilterXObject *res = filterx_expr_eval(coalesce); + cr_assert_null(res); + + // rhs expr eval errors must remain intact + const gchar *last_error = filterx_eval_get_last_error(); + cr_assert_not_null(last_error); + cr_assert_str_eq(error_msg, last_error); + + filterx_expr_unref(coalesce); + filterx_object_unref(res); +} + +Test(expr_null_coalesce, test_coalescing_keep_rhs_eval_error_on_double_fail) +{ + FilterXExpr *err_expr_lhs = filterx_dummy_error_new("lhs error"); + + const gchar *error_msg = "rhs error"; + FilterXExpr *err_expr_rhs = filterx_dummy_error_new(error_msg); + + // passing errorous expressions + FilterXExpr *coalesce = filterx_null_coalesce_new(err_expr_lhs, err_expr_rhs); + cr_assert(coalesce); + + // null_coalesce returns null, since both lhs and rhs fails + FilterXObject *res = filterx_expr_eval(coalesce); + cr_assert_null(res); + + // rhs expr eval errors must remain intact + const gchar *last_error = filterx_eval_get_last_error(); + cr_assert_not_null(last_error); + cr_assert_str_eq(error_msg, last_error); + + filterx_expr_unref(coalesce); + filterx_object_unref(res); +} + +static void +setup(void) +{ + app_startup(); + init_libtest_filterx(); +} + +static void +teardown(void) +{ + scratch_buffers_explicit_gc(); + deinit_libtest_filterx(); + app_shutdown(); +} + +TestSuite(expr_null_coalesce, .init = setup, .fini = teardown); diff --git a/libtest/filterx-lib.c b/libtest/filterx-lib.c index b8e8f63bc4..879e1bb298 100644 --- a/libtest/filterx-lib.c +++ b/libtest/filterx-lib.c @@ -139,10 +139,39 @@ filterx_non_literal_new(FilterXObject *object) return shorthand; } -FilterXExpr * -filterx_error_expr_new(void) +typedef struct _FilterXDummyError FilterXDummyError; + +struct _FilterXDummyError +{ + FilterXExpr super; + gchar *msg; +}; + +static FilterXObject * +_eval(FilterXExpr *s) +{ + FilterXDummyError *self = (FilterXDummyError *)s; + filterx_eval_push_error(self->msg, s, NULL); + return NULL; +} + +static void +_free(FilterXExpr *s) { - return filterx_literal_new(NULL); + FilterXDummyError *self = (FilterXDummyError *)s; + g_free(self->msg); + filterx_expr_free_method(s); +} + +FilterXExpr * +filterx_dummy_error_new(const gchar *msg) +{ + FilterXDummyError *self = g_new0(FilterXDummyError, 1); + self->msg = g_strdup(msg); + filterx_expr_init_instance(&self->super); + self->super.eval = _eval; + self->super.free_fn = _free; + return &self->super; } static struct diff --git a/libtest/filterx-lib.h b/libtest/filterx-lib.h index ed63c64ff6..af6168b315 100644 --- a/libtest/filterx-lib.h +++ b/libtest/filterx-lib.h @@ -42,7 +42,7 @@ const gchar *filterx_test_unknown_object_marshaled_repr(gssize *len); const gchar *filterx_test_unknown_object_repr(gssize *len); FilterXExpr *filterx_non_literal_new(FilterXObject *object); -FilterXExpr *filterx_error_expr_new(void); +FilterXExpr *filterx_dummy_error_new(const gchar *msg); void init_libtest_filterx(void); void deinit_libtest_filterx(void); diff --git a/modules/kvformat/tests/test_filterx_func_format_kv.c b/modules/kvformat/tests/test_filterx_func_format_kv.c index ea4bf08af6..ade5e928e1 100644 --- a/modules/kvformat/tests/test_filterx_func_format_kv.c +++ b/modules/kvformat/tests/test_filterx_func_format_kv.c @@ -99,7 +99,7 @@ Test(filterx_func_format_kv, test_invalid_args) /* error value_separator */ args = g_list_append(args, filterx_function_arg_new(NULL, filterx_literal_new(filterx_test_dict_new()))); - args = g_list_append(args, filterx_function_arg_new("value_separator", filterx_error_expr_new())); + args = g_list_append(args, filterx_function_arg_new("value_separator", filterx_dummy_error_new(""))); _assert_format_kv_init_fail(args); args = NULL; @@ -124,7 +124,7 @@ Test(filterx_func_format_kv, test_invalid_args) /* error pair_separator */ args = g_list_append(args, filterx_function_arg_new(NULL, filterx_literal_new(filterx_test_dict_new()))); - args = g_list_append(args, filterx_function_arg_new("pair_separator", filterx_error_expr_new())); + args = g_list_append(args, filterx_function_arg_new("pair_separator", filterx_dummy_error_new(""))); _assert_format_kv_init_fail(args); args = NULL; diff --git a/tests/light/functional_tests/filterx/test_filterx.py b/tests/light/functional_tests/filterx/test_filterx.py index 91e8fbd849..42dc98c259 100644 --- a/tests/light/functional_tests/filterx/test_filterx.py +++ b/tests/light/functional_tests/filterx/test_filterx.py @@ -1534,3 +1534,149 @@ def test_unset_empties(config, syslog_ng): assert file_true.get_stats()["processed"] == 1 assert "processed" not in file_false.get_stats() assert file_true.read_log() == "[{},{},[],[]]\n" + + +def test_null_coalesce_use_default_on_null(config, syslog_ng): + (file_true, file_false) = create_config( + config, r""" + x = null; + y = "bar"; + $MSG = x ?? y; + """, + ) + syslog_ng.start(config) + + assert file_true.get_stats()["processed"] == 1 + assert "processed" not in file_false.get_stats() + assert file_true.read_log() == "bar\n" + + +def test_null_coalesce_use_default_on_error_and_supress_error(config, syslog_ng): + (file_true, file_false) = create_config( + config, r""" + y = "bar"; + $MSG = len(3.14) ?? y; # in-line expression evaluation is mandatory. the error would be propagated other way + """, + ) + syslog_ng.start(config) + + assert file_true.get_stats()["processed"] == 1 + assert "processed" not in file_false.get_stats() + assert file_true.read_log() == "bar\n" + + +def test_null_coalesce_get_happy_paths(config, syslog_ng): + (file_true, file_false) = create_config( + config, r""" + data = json({"foo":"1", "bar":"2", "baz":"3"}); + def = "default"; + key = "bar"; + $MSG = json(); + + $MSG.a = data[key] ?? def; + $MSG.b = key ?? def; + """, + ) + syslog_ng.start(config) + + assert file_true.get_stats()["processed"] == 1 + assert "processed" not in file_false.get_stats() + assert file_true.read_log() == '{"a":"2","b":"bar"}\n' + + +def test_null_coalesce_get_subscript_error(config, syslog_ng): + (file_true, file_false) = create_config( + config, r""" + data = json({"foo":"1", "bar":"2", "baz":"3"}); + def = "default"; + key = "missing_key"; + $MSG = data[key] ?? def; + """, + ) + syslog_ng.start(config) + + assert file_true.get_stats()["processed"] == 1 + assert "processed" not in file_false.get_stats() + assert file_true.read_log() == "default\n" + + +def test_null_coalesce_use_nested_coalesce(config, syslog_ng): + (file_true, file_false) = create_config( + config, r""" + data = json({"foo":"1", "bar":"2", "baz":"3"}); + def = "default"; + key1 = "missing_key1"; + key2 = "missing_key2"; + $MSG = data[key1] ?? data[key2] ?? def; + """, + ) + syslog_ng.start(config) + + assert file_true.get_stats()["processed"] == 1 + assert "processed" not in file_false.get_stats() + assert file_true.read_log() == "default\n" + + +def test_null_coalesce_use_nested_coalesce_return_mid_match(config, syslog_ng): + (file_true, file_false) = create_config( + config, r""" + data = json({"foo":"1", "bar":"2", "baz":"3"}); + def = "default"; + key1 = "missing_key1"; + key2 = "baz"; + $MSG = data[key1] ?? data[key2] ?? def; + """, + ) + syslog_ng.start(config) + + assert file_true.get_stats()["processed"] == 1 + assert "processed" not in file_false.get_stats() + assert file_true.read_log() == "3\n" + + +def test_null_coalesce_do_not_supress_last_error(config, syslog_ng): + (file_true, file_false) = create_config( + config, r""" + data = json({"foo":"1", "bar":"2", "baz":"3"}); + def = "default"; + key1 = "missing_key1"; + key2 = "missing_key2"; + key3 = "missing_key3"; + $MSG = data[key1] ?? data[key2] ?? data[key3]; + """, + ) + syslog_ng.start(config) + + assert file_false.get_stats()["processed"] == 1 + assert "processed" not in file_true.get_stats() + + +def test_null_coalesce_precedence_versus_ternary(config, syslog_ng): + (file_true, file_false) = create_config( + config, r""" + data = json({"foo":"1", "bar":"2", "baz":"3"}); + def = "default"; + $MSG = json(); + + # according to c# and python null coalesce have higher precedence + + # a = ( false ?? isset(data["foo"]) ) ? data["foo"] : def; + $MSG.a = false ?? isset(data["foo"]) ? data["foo"] : def; + + # b = true ? ( data["wrong"] ?? data["bar"] ) : def; + $MSG.b = true ? data["wrong"] ?? data["bar"] : def; + + $MSG.c = data["foo"] ? data["bar"] ?? data["baz"] : def; + + # $MSG.d = false ? ( data["foo"] ?? data["bar"] ) : ( data["wrong"] ?? data["baz"] ) ; + $MSG.d = false ? data["foo"] ?? data["bar"] : data["wrong"] ?? data["baz"]; + + # ternary default match value + $MSG.e = data["wrong"] ?? data["foo"] ? : def; + """, + ) + syslog_ng.start(config) + + assert file_true.get_stats()["processed"] == 1 + assert "processed" not in file_false.get_stats() + assert file_true.read_log() == '{"a":"default","b":"2","c":"2","d":"3","e":"1"}\n'