diff --git a/.gitignore b/.gitignore index 442c94d..32b919a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .coveralls.yml +.cache/ .vscode .vs diff --git a/README.md b/README.md index 9b213e1..c3abe57 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) { diff --git a/include/inja/environment.hpp b/include/inja/environment.hpp index 64e3f6b..f30f855 100644 --- a/include/inja/environment.hpp +++ b/include/inja/environment.hpp @@ -8,13 +8,14 @@ #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 +26,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; @@ -179,10 +215,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{}); + } } /*! diff --git a/include/inja/utils.hpp b/include/inja/utils.hpp index e4e2856..08158be 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/single_include/inja/inja.hpp b/single_include/inja/inja.hpp index db597fa..32e4ce8 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{}); + } } /*! diff --git a/test/test-functions.cpp b/test/test-functions.cpp index 6ed2df0..0b086d5 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,27 +257,31 @@ 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) { }); + 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) == ""); + 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"); @@ -284,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) {