Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
.coveralls.yml
.cache/
.vscode
.vs

Expand Down
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,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) {
Expand Down
76 changes: 72 additions & 4 deletions include/inja/environment.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,14 @@
#include <string>
#include <string_view>

#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 {

Expand All @@ -25,6 +26,41 @@ class Environment {
FunctionStorage function_storage;
TemplateStorage template_storage;

template <class Arg>
static Arg get_callback_argument(const Arguments &args, size_t index) {
using BasicArg = std::remove_const_t<
std::remove_pointer_t<std::remove_reference_t<std::decay_t<Arg>>>>;

static constexpr bool check =
std::is_const_v<std::remove_reference_t<Arg>> ||
std::is_same_v<BasicArg, Arg>;
static_assert(check, "Arguments should be either const& or a value type");

if constexpr (std::is_same_v<BasicArg, json>) {
return *args[index];
} else if constexpr (std::is_lvalue_reference_v<Arg>) {
return args[index]->get_ref<Arg>();
} else {
return args[index]->get<Arg>();
}
}

template <class Ret, class Func, class... Args, size_t... Is>
void add_callback_closure(const std::string &name, Func func,
function_signature::ArgsList<Args...> /*args*/,
std::index_sequence<Is...> /*seq*/) {
add_callback(name, sizeof...(Args),
[func = std::move(func)] //
([[maybe_unused]] const Arguments &args) -> json {
if constexpr (std::is_same_v<Ret, void>) {
func(get_callback_argument<Args>(args, Is)...);
return {};
} else {
return func(get_callback_argument<Args>(args, Is)...);
}
});
}

protected:
LexerConfig lexer_config;
ParserConfig parser_config;
Expand Down Expand Up @@ -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 <class Callback>
void add_callback(const std::string &name, Callback callback) {
static constexpr auto get_sig = [] {
if constexpr (std::is_class_v<Callback>) {
return function_signature::Get<decltype(&Callback::operator())> {};
} else {
return function_signature::Get<Callback>{};
}
};
using Sig = decltype(get_sig());
static constexpr size_t num_args =
std::tuple_size_v<typename Sig::ArgsTuple>;

static constexpr auto is_arguments_vector = [] {
if constexpr (num_args == 1) {
return std::is_same_v<
std::remove_cv_t<std::remove_reference_t<
std::tuple_element_t<0, typename Sig::ArgsTuple>>>,
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<typename Sig::Ret>(
name, std::move(callback), typename Sig::ArgsList{},
std::make_index_sequence<num_args>{});
}
}

/*!
Expand Down
23 changes: 23 additions & 0 deletions include/inja/utils.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,29 @@ inline bool starts_with(std::string_view view, std::string_view prefix) {
}
} // namespace string_view

namespace function_signature {
template <class... Args> struct ArgsList {};
template <class Func> struct Get {};
template <class R, class... A> //
struct Get<R (*)(A...)> {
using Ret = R;
using ArgsList = ArgsList<A...>;
using ArgsTuple = std::tuple<A...>;
};
template <class R, class C, class... A> //
struct Get<R (C::*)(A...)> {
using Ret = R;
using ArgsList = ArgsList<A...>;
using ArgsTuple = std::tuple<A...>;
};
template <class R, class C, class... A> //
struct Get<R (C::*)(A...) const> {
using Ret = R;
using ArgsList = ArgsList<A...>;
using ArgsTuple = std::tuple<A...>;
};
} // 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);
Expand Down
102 changes: 97 additions & 5 deletions single_include/inja/inja.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,6 @@ std::abort(); \
#include <string>
#include <string_view>

// #include "json.hpp"

// #include "config.hpp"
#ifndef INCLUDE_INJA_CONFIG_HPP_
#define INCLUDE_INJA_CONFIG_HPP_
Expand Down Expand Up @@ -336,6 +334,29 @@ inline bool starts_with(std::string_view view, std::string_view prefix) {
}
} // namespace string_view

namespace function_signature {
template <class... Args> struct ArgsList {};
template <class Func> struct Get {};
template <class R, class... A> //
struct Get<R (*)(A...)> {
using Ret = R;
using ArgsList = ArgsList<A...>;
using ArgsTuple = std::tuple<A...>;
};
template <class R, class C, class... A> //
struct Get<R (C::*)(A...)> {
using Ret = R;
using ArgsList = ArgsList<A...>;
using ArgsTuple = std::tuple<A...>;
};
template <class R, class C, class... A> //
struct Get<R (C::*)(A...) const> {
using Ret = R;
using ArgsList = ArgsList<A...>;
using ArgsTuple = std::tuple<A...>;
};
} // 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);
Expand Down Expand Up @@ -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_
Expand Down Expand Up @@ -2790,6 +2813,8 @@ class Renderer : public NodeVisitor {

// #include "throw.hpp"

// #include "utils.hpp"


namespace inja {

Expand All @@ -2800,6 +2825,41 @@ class Environment {
FunctionStorage function_storage;
TemplateStorage template_storage;

template <class Arg>
static Arg get_callback_argument(const Arguments &args, size_t index) {
using BasicArg = std::remove_const_t<
std::remove_pointer_t<std::remove_reference_t<std::decay_t<Arg>>>>;

static constexpr bool check =
std::is_const_v<std::remove_reference_t<Arg>> ||
std::is_same_v<BasicArg, Arg>;
static_assert(check, "Arguments should be either const& or a value type");

if constexpr (std::is_same_v<BasicArg, json>) {
return *args[index];
} else if constexpr (std::is_lvalue_reference_v<Arg>) {
return args[index]->get_ref<Arg>();
} else {
return args[index]->get<Arg>();
}
}

template <class Ret, class Func, class... Args, size_t... Is>
void add_callback_closure(const std::string &name, Func func,
function_signature::ArgsList<Args...> /*args*/,
std::index_sequence<Is...> /*seq*/) {
add_callback(name, sizeof...(Args),
[func = std::move(func)] //
([[maybe_unused]] const Arguments &args) -> json {
if constexpr (std::is_same_v<Ret, void>) {
func(get_callback_argument<Args>(args, Is)...);
return {};
} else {
return func(get_callback_argument<Args>(args, Is)...);
}
});
}

protected:
LexerConfig lexer_config;
ParserConfig parser_config;
Expand Down Expand Up @@ -2954,10 +3014,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 <class Callback>
void add_callback(const std::string &name, Callback callback) {
static constexpr auto get_sig = [] {
if constexpr (std::is_class_v<Callback>) {
return function_signature::Get<decltype(&Callback::operator())> {};
} else {
return function_signature::Get<Callback>{};
}
};
using Sig = decltype(get_sig());
static constexpr size_t num_args =
std::tuple_size_v<typename Sig::ArgsTuple>;

static constexpr auto is_arguments_vector = [] {
if constexpr (num_args == 1) {
return std::is_same_v<
std::remove_cv_t<std::remove_reference_t<
std::tuple_element_t<0, typename Sig::ArgsTuple>>>,
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<typename Sig::Ret>(
name, std::move(callback), typename Sig::ArgsList{},
std::make_index_sequence<num_args>{});
}
}

/*!
Expand Down
31 changes: 20 additions & 11 deletions test/test-functions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -246,35 +248,40 @@ 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<double>();
auto number2 = args.at(1)->get<double>();
return number1 * number2;
});

env.add_callback("multiply", 3, [](inja::Arguments args) {
double number1 = args.at(0)->get<double>();
double number2 = args.at(1)->get<double>();
double number3 = args.at(2)->get<double>();
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<std::string>();
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");
Expand All @@ -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) {
Expand Down