From b74caa151ff022443e715dbc3efccb8282019986 Mon Sep 17 00:00:00 2001 From: ochafik Date: Mon, 30 Dec 2024 02:46:18 +0000 Subject: [PATCH] List & object pop() method --- include/minja/minja.hpp | 38 ++++++++++++++++++++++ tests/test-syntax.cpp | 72 ++++++++++++++++++++++++----------------- 2 files changed, 80 insertions(+), 30 deletions(-) diff --git a/include/minja/minja.hpp b/include/minja/minja.hpp index 9d9a1a0..5735692 100644 --- a/include/minja/minja.hpp +++ b/include/minja/minja.hpp @@ -206,6 +206,38 @@ class Value : public std::enable_shared_from_this { throw std::runtime_error("Value is not an array: " + dump()); array_->push_back(v); } + Value pop(const Value& index) { + if (is_array()) { + if (array_->empty()) + throw std::runtime_error("pop from empty list"); + if (index.is_null()) { + auto ret = array_->back(); + array_->pop_back(); + return ret; + } else if (!index.is_number_integer()) { + throw std::runtime_error("pop index must be an integer: " + index.dump()); + } else { + auto i = index.get(); + if (i < 0 || i >= static_cast(array_->size())) + throw std::runtime_error("pop index out of range: " + index.dump()); + auto it = array_->begin() + (i < 0 ? array_->size() + i : i); + auto ret = *it; + array_->erase(it); + return ret; + } + } else if (is_object()) { + if (!index.is_hashable()) + throw std::runtime_error("Unashable type: " + index.dump()); + auto it = object_->find(index.primitive_); + if (it == object_->end()) + throw std::runtime_error("Key not found: " + index.dump()); + auto ret = it->second; + object_->erase(it); + return ret; + } else { + throw std::runtime_error("Value is not an array or object: " + dump()); + } + } Value get(const Value& key) { if (array_) { if (!key.is_number_integer()) { @@ -1349,6 +1381,9 @@ class MethodCallExpr : public Expression { vargs.expectArgs("append method", {1, 1}, {0, 0}); obj.push_back(vargs.args[0]); return Value(); + } else if (method->get_name() == "pop") { + vargs.expectArgs("pop method", {0, 1}, {0, 0}); + return obj.pop(vargs.args.empty() ? Value() : vargs.args[0]); } else if (method->get_name() == "insert") { vargs.expectArgs("insert method", {2, 2}, {0, 0}); auto index = vargs.args[0].get(); @@ -1364,6 +1399,9 @@ class MethodCallExpr : public Expression { result.push_back(Value::array({key, obj.at(key)})); } return result; + } else if (method->get_name() == "pop") { + vargs.expectArgs("pop method", {1, 1}, {0, 0}); + return obj.pop(vargs.args[0]); } else if (method->get_name() == "get") { vargs.expectArgs("get method", {1, 2}, {0, 0}); auto key = vargs.args[0]; diff --git a/tests/test-syntax.cpp b/tests/test-syntax.cpp index fb00303..41f38ee 100644 --- a/tests/test-syntax.cpp +++ b/tests/test-syntax.cpp @@ -24,7 +24,7 @@ static std::string render_python(const std::string & template_str, const json & {"keep_trailing_newline", options.keep_trailing_newline}, }}, }; - { + { std::ofstream of("data.json"); of << data.dump(2); of.close(); @@ -46,21 +46,11 @@ static std::string render_python(const std::string & template_str, const json & static std::string render(const std::string & template_str, const json & bindings, const minja::Options & options) { if (getenv("USE_JINJA2")) { - try { - return render_python(template_str, bindings, options); - } catch (const std::exception & e) { - std::cerr << "ERROR: " + std::string(e.what()); - } + return render_python(template_str, bindings, options); } auto root = minja::Parser::parse(template_str, options); auto context = minja::Context::make(bindings); - std::string actual; - try { - actual = root->render(context); - } catch (const std::runtime_error & e) { - actual = "ERROR: " + std::string(e.what()); - } - return actual; + return root->render(context); } const minja::Options lstrip_blocks { @@ -80,6 +70,9 @@ const minja::Options lstrip_trim_blocks { }; TEST(SyntaxTest, SimpleCases) { + auto ThrowsWithSubstr = [](const std::string & expected_substr) { + return testing::Throws(Property(&std::runtime_error::what, testing::HasSubstr(expected_substr))); + }; // EXPECT_EQ( // "\r\nhey\r\nho!", // render("\r\n{{ 'hey\r\nho!' }}\r\n", {}, {})); @@ -93,12 +86,12 @@ TEST(SyntaxTest, SimpleCases) { EXPECT_EQ("\n", render(" {% if True %}\n {% endif %}", {}, lstrip_blocks)); EXPECT_EQ("", render(" {% if True %}\n {% endif %}", {}, lstrip_trim_blocks)); EXPECT_EQ(" ", render(" {% if True %}\n {% endif %}", {}, trim_blocks)); - + EXPECT_EQ(" ", render(" {% set _ = 1 %} ", {}, {})); EXPECT_EQ(" ", render(" {% set _ = 1 %} ", {}, lstrip_blocks)); EXPECT_EQ(" ", render(" {% set _ = 1 %} ", {}, trim_blocks)); EXPECT_EQ(" ", render(" {% set _ = 1 %} ", {}, lstrip_trim_blocks)); - + EXPECT_EQ(" \n \n ", render(" \n {% set _ = 1 %} \n ", {}, {})); EXPECT_EQ(" \n \n ", render(" \n {% set _ = 1 %} \n ", {}, lstrip_blocks)); EXPECT_EQ(" \n \n ", render(" \n {% set _ = 1 %} \n ", {}, trim_blocks)); @@ -322,6 +315,22 @@ TEST(SyntaxTest, SimpleCases) { ) ); } + EXPECT_EQ( + "[0, 1, 2][0, 2]", + render(R"( + {%- set o = [0, 1, 2, 3] -%} + {%- set _ = o.pop() -%} + {{- o | tojson -}} + {%- set _ = o.pop(1) -%} + {{- o | tojson -}} + )", {}, {})); + EXPECT_EQ( + R"({"y": 2})", + render(R"( + {%- set o = {"x": 1, "y": 2} -%} + {%- set _ = o.pop("x") -%} + {{- o | tojson -}} + )", {}, {})); EXPECT_EQ( R"(<, >, &, ")", render(R"( @@ -360,7 +369,7 @@ TEST(SyntaxTest, SimpleCases) { {{- values -}} {%- endmacro -%} {{- foo() }} {{ foo() -}})", {}, {})); - + if (!getenv("USE_JINJA2")) { EXPECT_EQ( "[]", @@ -466,24 +475,27 @@ TEST(SyntaxTest, SimpleCases) { "", render("{% if 1 %}{% elif 1 %}{% else %}{% endif %}", {}, {})); + if (!getenv("USE_JINJA2")) { - auto expect_throws_with_message_substr = [](const std::function & fn, const std::string & expected_substr) { - EXPECT_THAT([=]() { fn(); }, testing::Throws(Property(&std::runtime_error::what, testing::HasSubstr(expected_substr)))); - }; + // TODO: capture stderr from jinja2 and test these. + + EXPECT_THAT([]() { render("{%- set _ = [].pop() -%}", {}, {}); }, ThrowsWithSubstr("pop from empty list")); + EXPECT_THAT([]() { render("{%- set _ = {}.pop() -%}", {}, {}); }, ThrowsWithSubstr("pop")); + EXPECT_THAT([]() { render("{%- set _ = {}.pop('foooo') -%}", {}, {}); }, ThrowsWithSubstr("foooo")); - expect_throws_with_message_substr([]() { render("{% else %}", {}, {}); }, "Unexpected else"); + EXPECT_THAT([]() { render("{% else %}", {}, {}); }, ThrowsWithSubstr("Unexpected else")); - expect_throws_with_message_substr([]() { render("{% else %}", {}, {}); }, "Unexpected else"); - expect_throws_with_message_substr([]() { render("{% endif %}", {}, {}); }, "Unexpected endif"); - expect_throws_with_message_substr([]() { render("{% elif 1 %}", {}, {}); }, "Unexpected elif"); - expect_throws_with_message_substr([]() { render("{% endfor %}", {}, {}); }, "Unexpected endfor"); - expect_throws_with_message_substr([]() { render("{% endfilter %}", {}, {}); }, "Unexpected endfilter"); + EXPECT_THAT([]() { render("{% else %}", {}, {}); }, ThrowsWithSubstr("Unexpected else")); + EXPECT_THAT([]() { render("{% endif %}", {}, {}); }, ThrowsWithSubstr("Unexpected endif")); + EXPECT_THAT([]() { render("{% elif 1 %}", {}, {}); }, ThrowsWithSubstr("Unexpected elif")); + EXPECT_THAT([]() { render("{% endfor %}", {}, {}); }, ThrowsWithSubstr("Unexpected endfor")); + EXPECT_THAT([]() { render("{% endfilter %}", {}, {}); }, ThrowsWithSubstr("Unexpected endfilter")); - expect_throws_with_message_substr([]() { render("{% if 1 %}", {}, {}); }, "Unterminated if"); - expect_throws_with_message_substr([]() { render("{% for x in 1 %}", {}, {}); }, "Unterminated for"); - expect_throws_with_message_substr([]() { render("{% if 1 %}{% else %}", {}, {}); }, "Unterminated if"); - expect_throws_with_message_substr([]() { render("{% if 1 %}{% else %}{% elif 1 %}{% endif %}", {}, {}); }, "Unterminated if"); - expect_throws_with_message_substr([]() { render("{% filter trim %}", {}, {}); }, "Unterminated filter"); + EXPECT_THAT([]() { render("{% if 1 %}", {}, {}); }, ThrowsWithSubstr("Unterminated if")); + EXPECT_THAT([]() { render("{% for x in 1 %}", {}, {}); }, ThrowsWithSubstr("Unterminated for")); + EXPECT_THAT([]() { render("{% if 1 %}{% else %}", {}, {}); }, ThrowsWithSubstr("Unterminated if")); + EXPECT_THAT([]() { render("{% if 1 %}{% else %}{% elif 1 %}{% endif %}", {}, {}); }, ThrowsWithSubstr("Unterminated if")); + EXPECT_THAT([]() { render("{% filter trim %}", {}, {}); }, ThrowsWithSubstr("Unterminated filter")); } EXPECT_EQ(