From 6c7e76b261d2a37a424fb981efa97818d9456dfc Mon Sep 17 00:00:00 2001 From: Stat1cV01D <1160915+Stat1cV01D@users.noreply.github.com> Date: Sat, 4 Oct 2025 17:23:35 +0700 Subject: [PATCH 1/7] Deduce argument size and types from lambda signature This commit adds an ability to use lambda in a more traditional scenario with a lambda and a predefined number of arguments. This commit reuses `add_callback` function to extend its functionality. It reuses existing methods and should provide backward compatibility with the existing use cases. --- .gitignore | 1 + include/inja/environment.hpp | 66 +++++++++++++++++++++++++++++++++--- include/inja/utils.hpp | 23 +++++++++++++ test/test-functions.cpp | 24 +++++++------ 4 files changed, 99 insertions(+), 15 deletions(-) diff --git a/.gitignore b/.gitignore index 442c94d5..32b919ac 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .coveralls.yml +.cache/ .vscode .vs diff --git a/include/inja/environment.hpp b/include/inja/environment.hpp index 64e3f6bc..69152acc 100644 --- a/include/inja/environment.hpp +++ b/include/inja/environment.hpp @@ -7,14 +7,17 @@ #include #include #include +#include +#include -#include "json.hpp" #include "config.hpp" #include "function_storage.hpp" +#include "json.hpp" #include "parser.hpp" #include "renderer.hpp" #include "template.hpp" #include "throw.hpp" +#include "utils.hpp" namespace inja { @@ -25,6 +28,30 @@ class Environment { FunctionStorage function_storage; TemplateStorage template_storage; + template + static Arg get_callback_argument(const Arguments &args, size_t index) { + if constexpr (std::is_lvalue_reference_v) { + return args[index]->get_ref(); + } else { + return args[index]->get(); + } + } + + template + void add_callback_closure(const std::string &name, Func func, + function_signature::ArgsList /*args*/, + std::index_sequence /*seq*/) { + add_callback(name, sizeof...(Args), + [func = std::move(func)](const Arguments &args) -> json { + if constexpr (std::is_same_v) { + func(get_callback_argument(args, Is)...); + return {}; + } else { + return func(get_callback_argument(args, Is)...); + } + }); + } + protected: LexerConfig lexer_config; ParserConfig parser_config; @@ -179,10 +206,41 @@ class Environment { } /*! - @brief Adds a variadic callback + @brief Adds a callback */ - void add_callback(const std::string& name, const CallbackFunction& callback) { - add_callback(name, -1, callback); + template + void add_callback(const std::string &name, Callback callback) { + constexpr auto get_sig = [] { + if constexpr (std::is_class_v) { + return function_signature::Get {}; + } else { + return function_signature::Get{}; + } + }; + using Sig = decltype(get_sig()); + constexpr size_t num_args = std::tuple_size_v; + + constexpr auto is_arguments_vector = [] { + if constexpr (num_args == 1) { + return std::is_same_v< + std::remove_cv_t>>, + Arguments>; + } else { + return false; + } + }; + + if constexpr (is_arguments_vector()) { + // If callback has the only argument of `Arguments` - fallback to adding a + // variadic callback + add_callback(name, -1, callback); + } else { + // If it has other arguments - use it in a closure + add_callback_closure( + name, std::move(callback), typename Sig::ArgsList{}, + std::make_index_sequence{}); + } } /*! diff --git a/include/inja/utils.hpp b/include/inja/utils.hpp index e4e28560..08158be6 100644 --- a/include/inja/utils.hpp +++ b/include/inja/utils.hpp @@ -31,6 +31,29 @@ inline bool starts_with(std::string_view view, std::string_view prefix) { } } // namespace string_view +namespace function_signature { +template struct ArgsList {}; +template struct Get {}; +template // +struct Get { + using Ret = R; + using ArgsList = ArgsList; + using ArgsTuple = std::tuple; +}; +template // +struct Get { + using Ret = R; + using ArgsList = ArgsList; + using ArgsTuple = std::tuple; +}; +template // +struct Get { + using Ret = R; + using ArgsList = ArgsList; + using ArgsTuple = std::tuple; +}; +} // namespace function_signature + inline SourceLocation get_source_location(std::string_view content, size_t pos) { // Get line and offset position (starts at 1:1) auto sliced = string_view::slice(content, 0, pos); diff --git a/test/test-functions.cpp b/test/test-functions.cpp index 6ed2df08..387bebd5 100644 --- a/test/test-functions.cpp +++ b/test/test-functions.cpp @@ -230,6 +230,8 @@ TEST_CASE("assignments") { CHECK(env.render("{% set v1 = \"a\" %}{% set v2 = \"b\" %}{% set var = v1 + v2 %}{{ var }}", data) == "ab"); } +void dummy_callback() {} + TEST_CASE("callbacks") { inja::Environment env; inja::json data; @@ -246,7 +248,8 @@ TEST_CASE("callbacks") { }); std::string greet = "Hello"; - env.add_callback("double-greetings", 0, [greet](inja::Arguments) { return greet + " " + greet + "!"; }); + env.add_callback("double-greetings", + [greet] { return greet + " " + greet + "!"; }); env.add_callback("multiply", 2, [](inja::Arguments args) { double number1 = args.at(0)->get(); @@ -254,17 +257,15 @@ TEST_CASE("callbacks") { return number1 * number2; }); - env.add_callback("multiply", 3, [](inja::Arguments args) { - double number1 = args.at(0)->get(); - double number2 = args.at(1)->get(); - double number3 = args.at(2)->get(); - return number1 * number2 * number3; - }); + env.add_callback("multiply", + [](double number1, double number2, double number3) { + return number1 * number2 * number3; + }); - env.add_callback("length", 1, [](inja::Arguments args) { - auto number1 = args.at(0)->get(); - return number1.length(); - }); + env.add_callback("length", + [](const std::string &number1) { return number1.length(); }); + + env.add_callback("dummy", dummy_callback); env.add_void_callback("log", 1, [](inja::Arguments) { @@ -275,6 +276,7 @@ TEST_CASE("callbacks") { CHECK(env.render("{{ double(age) }}", data) == "56"); CHECK(env.render("{{ half(age) }}", data) == "14"); CHECK(env.render("{{ log(age) }}", data) == ""); + CHECK(env.render("{{ dummy() }}", data) == ""); CHECK(env.render("{{ double-greetings }}", data) == "Hello Hello!"); CHECK(env.render("{{ double-greetings() }}", data) == "Hello Hello!"); CHECK(env.render("{{ multiply(4, 5) }}", data) == "20.0"); From 656c387d413ab6eae030eb6aef0da7d326d9ab79 Mon Sep 17 00:00:00 2001 From: Stat1cV01D <1160915+Stat1cV01D@users.noreply.github.com> Date: Sat, 4 Oct 2025 17:32:00 +0700 Subject: [PATCH 2/7] Remove extra headers (per static code analysis) --- include/inja/environment.hpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/include/inja/environment.hpp b/include/inja/environment.hpp index 69152acc..b75794eb 100644 --- a/include/inja/environment.hpp +++ b/include/inja/environment.hpp @@ -7,8 +7,6 @@ #include #include #include -#include -#include #include "config.hpp" #include "function_storage.hpp" From 5296a43e5f723196f2c8a8d975ec5748a5366ef2 Mon Sep 17 00:00:00 2001 From: Stat1cV01D <1160915+Stat1cV01D@users.noreply.github.com> Date: Sat, 4 Oct 2025 17:50:15 +0700 Subject: [PATCH 3/7] Minor fix for `get_ref` branch We need to remove reference first so that `const` is applied properly. Then we can add `&` back. --- include/inja/environment.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/inja/environment.hpp b/include/inja/environment.hpp index b75794eb..39c6d4eb 100644 --- a/include/inja/environment.hpp +++ b/include/inja/environment.hpp @@ -29,7 +29,7 @@ class Environment { template static Arg get_callback_argument(const Arguments &args, size_t index) { if constexpr (std::is_lvalue_reference_v) { - return args[index]->get_ref(); + return args[index]->get_ref &>(); } else { return args[index]->get(); } From d4a71650a174cfe5cc902e47c39b85e6d8080620 Mon Sep 17 00:00:00 2001 From: Stat1cV01D <1160915+Stat1cV01D@users.noreply.github.com> Date: Sat, 4 Oct 2025 20:18:21 +0700 Subject: [PATCH 4/7] Add support for `inja::json` parameters --- include/inja/environment.hpp | 14 ++++++++++++-- test/test-functions.cpp | 7 +++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/include/inja/environment.hpp b/include/inja/environment.hpp index 39c6d4eb..7aa78874 100644 --- a/include/inja/environment.hpp +++ b/include/inja/environment.hpp @@ -28,8 +28,18 @@ class Environment { template static Arg get_callback_argument(const Arguments &args, size_t index) { - if constexpr (std::is_lvalue_reference_v) { - return args[index]->get_ref &>(); + using BasicArg = std::remove_const_t< + std::remove_pointer_t>>>; + + constexpr bool is_valid_arg = + std::is_const_v> || + std::is_same_v; + static_assert(is_valid_arg, "Arguments should be either const& or a value type"); + + if constexpr (std::is_same_v) { + return *args[index]; + } else if constexpr (std::is_lvalue_reference_v) { + return args[index]->get_ref(); } else { return args[index]->get(); } diff --git a/test/test-functions.cpp b/test/test-functions.cpp index 387bebd5..0b086d59 100644 --- a/test/test-functions.cpp +++ b/test/test-functions.cpp @@ -271,8 +271,13 @@ TEST_CASE("callbacks") { }); + env.add_callback("get_arg_type", + [](const inja::json &input) { return input.type_name(); }); + env.add_callback("multiply", 0, [](inja::Arguments) { return 1.0; }); + env.add_callback("any_2_types", [](const inja::json &, inja::json) {}); + CHECK(env.render("{{ double(age) }}", data) == "56"); CHECK(env.render("{{ half(age) }}", data) == "14"); CHECK(env.render("{{ log(age) }}", data) == ""); @@ -286,6 +291,8 @@ TEST_CASE("callbacks") { CHECK(env.render("{{ multiply(5, length(\"t\")) }}", data) == "5.0"); CHECK(env.render("{{ multiply(3, 4, 5) }}", data) == "60.0"); CHECK(env.render("{{ multiply }}", data) == "1.0"); + CHECK(env.render("{{ get_arg_type(4) }}", data) == "number"); + CHECK(env.render("{{ get_arg_type(false) }}", data) == "boolean"); SUBCASE("Variadic") { env.add_callback("argmax", [](inja::Arguments& args) { From 71c0aed6402380bb75780b7261227a641ced28bf Mon Sep 17 00:00:00 2001 From: Stat1cV01D <1160915+Stat1cV01D@users.noreply.github.com> Date: Sat, 4 Oct 2025 20:18:33 +0700 Subject: [PATCH 5/7] Update README --- README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/README.md b/README.md index 9b213e19..c3abe577 100644 --- a/README.md +++ b/README.md @@ -315,6 +315,23 @@ env.add_callback("double-greetings", 0, [greet](Arguments args) { }); env.render("{{ double-greetings }}", data); // "Hello Hello!" ``` + +Another way to use callbacks is to list the expected arguments in the callback definition. +```.cpp +env.add_callback("double", [](int number) { + return 2 * number; +}); +``` + +Note that you can not use `auto`/template arguments in such functions. For this cases just use +`inja::json` and then get the json type +```.cpp +env.add_callback("get_arg_type", [](const inja::json& input) { + return input.type_name(); +}); +env.render("{{ get_arg_type(4) }}", data) == "number"; +``` + You can also add a void callback without return variable, e.g. for debugging: ```.cpp env.add_void_callback("log", 1, [greet](Arguments args) { From afe31e45cc5d5be6f7bbaad402ae5a7af01cfa1a Mon Sep 17 00:00:00 2001 From: Stat1cV01D <1160915+Stat1cV01D@users.noreply.github.com> Date: Sat, 4 Oct 2025 23:45:49 +0700 Subject: [PATCH 6/7] Fix MSVC warnings --- include/inja/environment.hpp | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/include/inja/environment.hpp b/include/inja/environment.hpp index 7aa78874..f30f8558 100644 --- a/include/inja/environment.hpp +++ b/include/inja/environment.hpp @@ -31,10 +31,10 @@ class Environment { using BasicArg = std::remove_const_t< std::remove_pointer_t>>>; - constexpr bool is_valid_arg = + static constexpr bool check = std::is_const_v> || std::is_same_v; - static_assert(is_valid_arg, "Arguments should be either const& or a value type"); + static_assert(check, "Arguments should be either const& or a value type"); if constexpr (std::is_same_v) { return *args[index]; @@ -50,7 +50,8 @@ class Environment { function_signature::ArgsList /*args*/, std::index_sequence /*seq*/) { add_callback(name, sizeof...(Args), - [func = std::move(func)](const Arguments &args) -> json { + [func = std::move(func)] // + ([[maybe_unused]] const Arguments &args) -> json { if constexpr (std::is_same_v) { func(get_callback_argument(args, Is)...); return {}; @@ -218,7 +219,7 @@ class Environment { */ template void add_callback(const std::string &name, Callback callback) { - constexpr auto get_sig = [] { + static constexpr auto get_sig = [] { if constexpr (std::is_class_v) { return function_signature::Get {}; } else { @@ -226,9 +227,10 @@ class Environment { } }; using Sig = decltype(get_sig()); - constexpr size_t num_args = std::tuple_size_v; + static constexpr size_t num_args = + std::tuple_size_v; - constexpr auto is_arguments_vector = [] { + static constexpr auto is_arguments_vector = [] { if constexpr (num_args == 1) { return std::is_same_v< std::remove_cv_t Date: Sat, 4 Oct 2025 23:49:11 +0700 Subject: [PATCH 7/7] Update single_include/inja.hpp --- single_include/inja/inja.hpp | 102 +++++++++++++++++++++++++++++++++-- 1 file changed, 97 insertions(+), 5 deletions(-) diff --git a/single_include/inja/inja.hpp b/single_include/inja/inja.hpp index db597fac..32e4ce8e 100644 --- a/single_include/inja/inja.hpp +++ b/single_include/inja/inja.hpp @@ -74,8 +74,6 @@ std::abort(); \ #include #include -// #include "json.hpp" - // #include "config.hpp" #ifndef INCLUDE_INJA_CONFIG_HPP_ #define INCLUDE_INJA_CONFIG_HPP_ @@ -336,6 +334,29 @@ inline bool starts_with(std::string_view view, std::string_view prefix) { } } // namespace string_view +namespace function_signature { +template struct ArgsList {}; +template struct Get {}; +template // +struct Get { + using Ret = R; + using ArgsList = ArgsList; + using ArgsTuple = std::tuple; +}; +template // +struct Get { + using Ret = R; + using ArgsList = ArgsList; + using ArgsTuple = std::tuple; +}; +template // +struct Get { + using Ret = R; + using ArgsList = ArgsList; + using ArgsTuple = std::tuple; +}; +} // namespace function_signature + inline SourceLocation get_source_location(std::string_view content, size_t pos) { // Get line and offset position (starts at 1:1) auto sliced = string_view::slice(content, 0, pos); @@ -922,6 +943,8 @@ struct RenderConfig { // #include "function_storage.hpp" +// #include "json.hpp" + // #include "parser.hpp" #ifndef INCLUDE_INJA_PARSER_HPP_ #define INCLUDE_INJA_PARSER_HPP_ @@ -2834,6 +2857,8 @@ class Renderer : public NodeVisitor { // #include "throw.hpp" +// #include "utils.hpp" + namespace inja { @@ -2844,6 +2869,41 @@ class Environment { FunctionStorage function_storage; TemplateStorage template_storage; + template + static Arg get_callback_argument(const Arguments &args, size_t index) { + using BasicArg = std::remove_const_t< + std::remove_pointer_t>>>; + + static constexpr bool check = + std::is_const_v> || + std::is_same_v; + static_assert(check, "Arguments should be either const& or a value type"); + + if constexpr (std::is_same_v) { + return *args[index]; + } else if constexpr (std::is_lvalue_reference_v) { + return args[index]->get_ref(); + } else { + return args[index]->get(); + } + } + + template + void add_callback_closure(const std::string &name, Func func, + function_signature::ArgsList /*args*/, + std::index_sequence /*seq*/) { + add_callback(name, sizeof...(Args), + [func = std::move(func)] // + ([[maybe_unused]] const Arguments &args) -> json { + if constexpr (std::is_same_v) { + func(get_callback_argument(args, Is)...); + return {}; + } else { + return func(get_callback_argument(args, Is)...); + } + }); + } + protected: LexerConfig lexer_config; ParserConfig parser_config; @@ -2998,10 +3058,42 @@ class Environment { } /*! - @brief Adds a variadic callback + @brief Adds a callback */ - void add_callback(const std::string& name, const CallbackFunction& callback) { - add_callback(name, -1, callback); + template + void add_callback(const std::string &name, Callback callback) { + static constexpr auto get_sig = [] { + if constexpr (std::is_class_v) { + return function_signature::Get {}; + } else { + return function_signature::Get{}; + } + }; + using Sig = decltype(get_sig()); + static constexpr size_t num_args = + std::tuple_size_v; + + static constexpr auto is_arguments_vector = [] { + if constexpr (num_args == 1) { + return std::is_same_v< + std::remove_cv_t>>, + Arguments>; + } else { + return false; + } + }; + + if constexpr (is_arguments_vector()) { + // If callback has the only argument of `Arguments` - fallback to adding a + // variadic callback + add_callback(name, -1, callback); + } else { + // If it has other arguments - use it in a closure + add_callback_closure( + name, std::move(callback), typename Sig::ArgsList{}, + std::make_index_sequence{}); + } } /*!