Add move command for moving (leaf)list instances
Change-Id: I0bff25209f74601a450c12a810200b3c124d65f2
diff --git a/src/ast_commands.cpp b/src/ast_commands.cpp
index c349e8c..1b89a6c 100644
--- a/src/ast_commands.cpp
+++ b/src/ast_commands.cpp
@@ -36,3 +36,8 @@
{
return this->m_path == b.m_path;
}
+
+bool move_::operator==(const move_& other) const
+{
+ return this->m_source == other.m_source && this->m_destination == other.m_destination;
+}
diff --git a/src/ast_commands.hpp b/src/ast_commands.hpp
index 5759eda..9c56d4f 100644
--- a/src/ast_commands.hpp
+++ b/src/ast_commands.hpp
@@ -11,6 +11,7 @@
#include <boost/spirit/home/x3/support/ast/position_tagged.hpp>
#include "ast_path.hpp"
#include "ast_values.hpp"
+#include "yang_move.hpp"
namespace x3 = boost::spirit::x3;
@@ -178,8 +179,35 @@
Datastore m_destination;
};
+enum class MoveMode {
+ Begin,
+ End,
+ Before,
+ After
+};
+
+struct move_ : x3::position_tagged {
+ static constexpr auto name = "move";
+ static constexpr auto shortHelp = "move - move (leaf)list instances around";
+ static constexpr auto longHelp = R"(
+ move <list-instance-path> begin
+ move <list-instance-path> end
+ move <list-instance-path> before <key>
+ move <list-instance-path> after <key>
+
+ Usage:
+ /> move mod:leaflist['abc'] begin
+ /> move mod:leaflist['def'] after 'abc'
+ /> move mod:interfaces['eth0'] after ['eth1'])";
+ bool operator==(const move_& b) const;
+
+ dataPath_ m_source;
+
+ std::variant<yang::move::Absolute, yang::move::Relative> m_destination;
+};
+
struct help_;
-using CommandTypes = boost::mpl::vector<cd_, commit_, copy_, create_, delete_, describe_, discard_, get_, help_, ls_, set_>;
+using CommandTypes = boost::mpl::vector<cd_, commit_, copy_, create_, delete_, describe_, discard_, get_, help_, ls_, move_, set_>;
struct help_ : x3::position_tagged {
static constexpr auto name = "help";
static constexpr auto shortHelp = "help - Print help for commands.";
@@ -225,3 +253,4 @@
BOOST_FUSION_ADAPT_STRUCT(discard_)
BOOST_FUSION_ADAPT_STRUCT(get_, m_path)
BOOST_FUSION_ADAPT_STRUCT(copy_, m_source, m_destination)
+BOOST_FUSION_ADAPT_STRUCT(move_, m_source, m_destination)
diff --git a/src/ast_handlers.hpp b/src/ast_handlers.hpp
index f344ac2..1f7dea6 100644
--- a/src/ast_handlers.hpp
+++ b/src/ast_handlers.hpp
@@ -94,7 +94,11 @@
parserContext.m_errorMsg += ".";
_pass(context) = false;
+ return;
}
+
+ // Clean up after listSuffix, in case someone wants to parse more listSuffixes
+ parserContext.m_tmpListKeys.clear();
}
template <typename Iterator, typename Exception, typename Context>
@@ -268,6 +272,8 @@
struct copy_class;
+struct move_class;
+
struct command_class {
template <typename Iterator, typename Exception, typename Context>
x3::error_handler_result on_error(Iterator&, Iterator const&, Exception const& x, Context const& context)
@@ -288,6 +294,7 @@
{
auto& parserContext = x3::get<parser_context_tag>(context);
parserContext.resetPath();
+ parserContext.m_tmpListPath = dataPath_{};
parserContext.m_tmpListKeys.clear();
parserContext.m_suggestions.clear();
}
diff --git a/src/datastore_access.hpp b/src/datastore_access.hpp
index 287ba94..05c1296 100644
--- a/src/datastore_access.hpp
+++ b/src/datastore_access.hpp
@@ -11,6 +11,7 @@
#include <map>
#include <optional>
#include <string>
+#include "yang_move.hpp"
#include "ast_values.hpp"
#include "list_instance.hpp"
@@ -37,6 +38,8 @@
class Schema;
+struct dataPath_;
+
class DatastoreAccess {
public:
using Tree = std::vector<std::pair<std::string, leaf_data_>>;
@@ -49,6 +52,7 @@
virtual void deleteListInstance(const std::string& path) = 0;
virtual void createLeafListInstance(const std::string& path) = 0;
virtual void deleteLeafListInstance(const std::string& path) = 0;
+ virtual void moveItem(const std::string& path, std::variant<yang::move::Absolute, yang::move::Relative> move) = 0;
virtual Tree executeRpc(const std::string& path, const Tree& input) = 0;
virtual std::shared_ptr<Schema> schema() = 0;
diff --git a/src/grammars.hpp b/src/grammars.hpp
index 253a16a..9f0baf2 100644
--- a/src/grammars.hpp
+++ b/src/grammars.hpp
@@ -27,6 +27,7 @@
x3::rule<describe_class, describe_> const describe = "describe";
x3::rule<help_class, help_> const help = "help";
x3::rule<copy_class, copy_> const copy = "copy";
+x3::rule<move_class, move_> const move = "move";
x3::rule<command_class, command_> const command = "command";
x3::rule<createCommandSuggestions_class, x3::unused_type> const createCommandSuggestions = "createCommandSuggestions";
@@ -134,11 +135,105 @@
auto const describe_def =
describe_::name >> space_separator > (dataPathListEnd | anyPath);
+struct mode_table : x3::symbols<MoveMode> {
+ mode_table()
+ {
+ add
+ ("after", MoveMode::After)
+ ("before", MoveMode::Before)
+ ("begin", MoveMode::Begin)
+ ("end", MoveMode::End);
+ }
+} const mode_table;
+
+struct move_absolute_table : x3::symbols<yang::move::Absolute> {
+ move_absolute_table()
+ {
+ add
+ ("begin", yang::move::Absolute::Begin)
+ ("end", yang::move::Absolute::End);
+ }
+} const move_absolute_table;
+
+struct move_relative_table : x3::symbols<yang::move::Relative::Position> {
+ move_relative_table()
+ {
+ add
+ ("before", yang::move::Relative::Position::Before)
+ ("after", yang::move::Relative::Position::After);
+ }
+} const move_relative_table;
+
+struct move_args : x3::parser<move_args> {
+ using attribute_type = move_;
+ template <typename It, typename Ctx, typename RCtx>
+ bool parse(It& begin, It end, Ctx const& ctx, RCtx& rctx, move_& attr) const
+ {
+ ParserContext& parserContext = x3::get<parser_context_tag>(ctx);
+ dataPath_ movePath;
+ auto movePathGrammar = listInstancePath | leafListElementPath;
+ auto res = movePathGrammar.parse(begin, end, ctx, rctx, attr.m_source);
+ if (!res) {
+ parserContext.m_errorMsg = "Expected source path here:";
+ return false;
+ }
+
+ // Try absolute move first.
+ res = (space_separator >> move_absolute_table).parse(begin, end, ctx, rctx, attr.m_destination);
+ if (res) {
+ // Absolute move parsing succeeded, we don't need to parse anything else.
+ return true;
+ }
+
+ // If absolute move didn't succeed, try relative.
+ attr.m_destination = yang::move::Relative{};
+ res = (space_separator >> move_relative_table).parse(begin, end, ctx, rctx, std::get<yang::move::Relative>(attr.m_destination).m_position);
+
+ if (!res) {
+ parserContext.m_errorMsg = "Expected a move position (begin, end, before, after) here:";
+ return false;
+ }
+
+ if (std::holds_alternative<leafListElement_>(attr.m_source.m_nodes.back().m_suffix)) {
+ leaf_data_ value;
+ res = (space_separator >> leaf_data).parse(begin, end, ctx, rctx, value);
+ if (res) {
+ std::get<yang::move::Relative>(attr.m_destination).m_path = {{".", value}};
+ }
+ } else {
+ ListInstance listInstance;
+ // The source list instance will be stored inside the parser context path.
+ // The source list instance will be full data path (with keys included).
+ // However, m_tmpListPath is supposed to store a path with a list without the keys.
+ // So, I pop the last listElement_ (which has the keys) and put in a list_ (which doesn't have the keys).
+ // Example: /mod:cont/protocols[name='ftp'] gets turned into /mod:cont/protocols
+ parserContext.m_tmpListPath = parserContext.currentDataPath();
+ parserContext.m_tmpListPath.m_nodes.pop_back();
+ auto list = list_{std::get<listElement_>(attr.m_source.m_nodes.back().m_suffix).m_name};
+ parserContext.m_tmpListPath.m_nodes.push_back(dataNode_{attr.m_source.m_nodes.back().m_prefix, list});
+
+ res = (space_separator >> listSuffix).parse(begin, end, ctx, rctx, listInstance);
+ if (res) {
+ std::get<yang::move::Relative>(attr.m_destination).m_path = listInstance;
+ }
+ }
+
+ if (!res) {
+ parserContext.m_errorMsg = "Expected a destination here:";
+ }
+
+ return res;
+ }
+} const move_args;
+
+auto const move_def =
+ move_::name >> space_separator >> move_args;
+
auto const createCommandSuggestions_def =
x3::eps;
auto const command_def =
- createCommandSuggestions >> x3::expect[cd | copy | create | delete_rule | set | commit | get | ls | discard | describe | help];
+ createCommandSuggestions >> x3::expect[cd | copy | create | delete_rule | set | commit | get | ls | discard | describe | help | move];
#if __clang__
#pragma GCC diagnostic pop
@@ -155,5 +250,6 @@
BOOST_SPIRIT_DEFINE(describe)
BOOST_SPIRIT_DEFINE(help)
BOOST_SPIRIT_DEFINE(copy)
+BOOST_SPIRIT_DEFINE(move)
BOOST_SPIRIT_DEFINE(command)
BOOST_SPIRIT_DEFINE(createCommandSuggestions)
diff --git a/src/interpreter.cpp b/src/interpreter.cpp
index 2d69e63..80e7a7a 100644
--- a/src/interpreter.cpp
+++ b/src/interpreter.cpp
@@ -195,6 +195,11 @@
}
}
+void Interpreter::operator()(const move_& move) const
+{
+ m_datastore.moveItem(pathToDataString(move.m_source, Prefixes::WhenNeeded), move.m_destination);
+}
+
struct commandLongHelpVisitor : boost::static_visitor<const char*> {
template <typename T>
auto constexpr operator()(boost::type<T>) const
diff --git a/src/interpreter.hpp b/src/interpreter.hpp
index 0dfe2b6..bd90a60 100644
--- a/src/interpreter.hpp
+++ b/src/interpreter.hpp
@@ -26,6 +26,7 @@
void operator()(const discard_&) const;
void operator()(const help_&) const;
void operator()(const copy_& copy) const;
+ void operator()(const move_& move) const;
private:
template <typename T>
diff --git a/src/netconf_access.cpp b/src/netconf_access.cpp
index 5e2e52a..e4a1859 100644
--- a/src/netconf_access.cpp
+++ b/src/netconf_access.cpp
@@ -140,6 +140,40 @@
doEditFromDataNode(node);
}
+struct impl_toYangInsert {
+ std::string operator()(yang::move::Absolute& absolute)
+ {
+ return absolute == yang::move::Absolute::Begin ? "first" : "last";
+ }
+ std::string operator()(yang::move::Relative& relative)
+ {
+ return relative.m_position == yang::move::Relative::Position::After ? "after" : "before";
+ }
+};
+
+std::string toYangInsert(std::variant<yang::move::Absolute, yang::move::Relative> move)
+{
+ return std::visit(impl_toYangInsert{}, move);
+}
+
+void NetconfAccess::moveItem(const std::string& source, std::variant<yang::move::Absolute, yang::move::Relative> move)
+{
+ auto node = m_schema->dataNodeFromPath(source);
+ auto sourceNode = *(node->find_path(source.c_str())->data().begin());
+ auto yangModule = m_schema->getYangModule("yang");
+ sourceNode->insert_attr(yangModule, "insert", toYangInsert(move).c_str());
+
+ if (std::holds_alternative<yang::move::Relative>(move)) {
+ auto relative = std::get<yang::move::Relative>(move);
+ if (m_schema->nodeType(source) == yang::NodeTypes::LeafList) {
+ sourceNode->insert_attr(yangModule, "value", leafDataToString(relative.m_path.at(".")).c_str());
+ } else {
+ sourceNode->insert_attr(yangModule, "key", instanceToString(node->node_module()->name(), relative.m_path).c_str());
+ }
+ }
+ doEditFromDataNode(sourceNode);
+}
+
void NetconfAccess::doEditFromDataNode(std::shared_ptr<libyang::Data_Node> dataNode)
{
auto data = dataNode->print_mem(LYD_XML, 0);
diff --git a/src/netconf_access.hpp b/src/netconf_access.hpp
index 0cc4eb4..3875911 100644
--- a/src/netconf_access.hpp
+++ b/src/netconf_access.hpp
@@ -41,6 +41,7 @@
void deleteListInstance(const std::string& path) override;
void createLeafListInstance(const std::string& path) override;
void deleteLeafListInstance(const std::string& path) override;
+ void moveItem(const std::string& path, std::variant<yang::move::Absolute, yang::move::Relative> move) override;
void commitChanges() override;
void discardChanges() override;
Tree executeRpc(const std::string& path, const Tree& input) override;
diff --git a/src/sysrepo_access.cpp b/src/sysrepo_access.cpp
index 3278766..d8a64da 100644
--- a/src/sysrepo_access.cpp
+++ b/src/sysrepo_access.cpp
@@ -289,6 +289,36 @@
}
}
+struct impl_toSrMoveOp {
+ sr_move_position_t operator()(yang::move::Absolute& absolute)
+ {
+ return absolute == yang::move::Absolute::Begin ? SR_MOVE_FIRST : SR_MOVE_LAST;
+ }
+ sr_move_position_t operator()(yang::move::Relative& relative)
+ {
+ return relative.m_position == yang::move::Relative::Position::After ? SR_MOVE_AFTER : SR_MOVE_BEFORE;
+ }
+};
+
+sr_move_position_t toSrMoveOp(std::variant<yang::move::Absolute, yang::move::Relative> move)
+{
+ return std::visit(impl_toSrMoveOp{}, move);
+}
+
+void SysrepoAccess::moveItem(const std::string& source, std::variant<yang::move::Absolute, yang::move::Relative> move)
+{
+ std::string destPathStr;
+ if (std::holds_alternative<yang::move::Relative>(move)) {
+ auto relative = std::get<yang::move::Relative>(move);
+ if (m_schema->nodeType(source) == yang::NodeTypes::LeafList) {
+ destPathStr = stripLeafListValueFromPath(source) + "[.='" + leafDataToString(relative.m_path.at(".")) + "']";
+ } else {
+ destPathStr = stripLastListInstanceFromPath(source) + instanceToString(m_schema->dataNodeFromPath(source)->node_module()->name(), relative.m_path);
+ }
+ }
+ m_session->move_item(source.c_str(), toSrMoveOp(move), destPathStr.c_str());
+}
+
void SysrepoAccess::commitChanges()
{
try {
diff --git a/src/sysrepo_access.hpp b/src/sysrepo_access.hpp
index 0ca7587..6e66eea 100644
--- a/src/sysrepo_access.hpp
+++ b/src/sysrepo_access.hpp
@@ -36,6 +36,7 @@
void deleteListInstance(const std::string& path) override;
void createLeafListInstance(const std::string& path) override;
void deleteLeafListInstance(const std::string& path) override;
+ void moveItem(const std::string& source, std::variant<yang::move::Absolute, yang::move::Relative> move) override;
Tree executeRpc(const std::string& path, const Tree& input) override;
std::shared_ptr<Schema> schema() override;
diff --git a/src/utils.cpp b/src/utils.cpp
index 941fa4e..cf789b7 100644
--- a/src/utils.cpp
+++ b/src/utils.cpp
@@ -235,3 +235,20 @@
res.erase(res.find_last_of('['));
return res;
}
+
+std::string stripLastListInstanceFromPath(const std::string& path)
+{
+ auto res = path;
+ res.erase(res.find_first_of('[', res.find_last_of('/')));
+ return res;
+}
+
+std::string instanceToString(const std::string& modName, const ListInstance& instance)
+{
+ std::string instanceStr;
+ for (const auto& [key, value] : instance) {
+ using namespace std::string_literals;
+ instanceStr += "[" + modName + ":" + key + "=" + escapeListKeyString(leafDataToString(value)) + "]";
+ }
+ return instanceStr;
+}
diff --git a/src/utils.hpp b/src/utils.hpp
index ecbf919..0196ff8 100644
--- a/src/utils.hpp
+++ b/src/utils.hpp
@@ -26,3 +26,6 @@
std::string leafDataToString(const leaf_data_ value);
schemaPath_ anyPathToSchemaPath(const boost::variant<dataPath_, schemaPath_, module_>& path);
std::string stripLeafListValueFromPath(const std::string& path);
+std::string stripLastListInstanceFromPath(const std::string& path);
+// The string includes module name prefixes.
+std::string instanceToString(const std::string& modName, const ListInstance& instance);
diff --git a/src/yang_move.hpp b/src/yang_move.hpp
new file mode 100644
index 0000000..0b1310c
--- /dev/null
+++ b/src/yang_move.hpp
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2020 CESNET, https://photonics.cesnet.cz/
+ *
+ * Written by Václav Kubernát <kubernat@cesnet.cz>
+ *
+*/
+#pragma once
+#include <variant>
+#include "list_instance.hpp"
+
+namespace yang::move {
+enum class Absolute {
+ Begin,
+ End
+};
+struct Relative {
+ bool operator==(const yang::move::Relative& other) const
+ {
+ return this->m_position == other.m_position && this->m_path == other.m_path;
+ }
+ enum class Position {
+ Before,
+ After
+ } m_position;
+ ListInstance m_path;
+};
+}
diff --git a/tests/command_completion.cpp b/tests/command_completion.cpp
index 8fe463f..45eb998 100644
--- a/tests/command_completion.cpp
+++ b/tests/command_completion.cpp
@@ -21,7 +21,7 @@
int expectedContextLength;
SECTION("no prefix")
{
- expectedCompletions = {"cd", "copy", "create", "delete", "set", "commit", "get", "ls", "discard", "help", "describe"};
+ expectedCompletions = {"cd", "copy", "create", "delete", "set", "commit", "get", "ls", "discard", "help", "describe", "move"};
expectedContextLength = 0;
SECTION("no space") {
input = "";
diff --git a/tests/datastore_access.cpp b/tests/datastore_access.cpp
index 07320c9..b9870f3 100644
--- a/tests/datastore_access.cpp
+++ b/tests/datastore_access.cpp
@@ -432,6 +432,189 @@
REQUIRE(datastore.getItems("/example-schema:leafInt16") == DatastoreAccess::Tree{});
}
+ SECTION("moving leaflist instances")
+ {
+ DatastoreAccess::Tree expected;
+ {
+ // sysrepo does this twice for some reason, it's possibly a bug
+ REQUIRE_CALL(mock, write("/example-schema:protocols", std::nullopt, "http"s)).TIMES(2);
+ REQUIRE_CALL(mock, write("/example-schema:protocols", std::nullopt, "ftp"s));
+ REQUIRE_CALL(mock, write("/example-schema:protocols", std::nullopt, "pop3"s));
+ REQUIRE_CALL(mock, write("/example-schema:protocols", "http"s, "ftp"s));
+ REQUIRE_CALL(mock, write("/example-schema:protocols", "ftp"s, "pop3"s));
+ datastore.createLeafListInstance("/example-schema:protocols[.='http']");
+ datastore.createLeafListInstance("/example-schema:protocols[.='ftp']");
+ datastore.createLeafListInstance("/example-schema:protocols[.='pop3']");
+ datastore.commitChanges();
+ expected = {
+ {"/example-schema:protocols", special_{SpecialValue::LeafList}},
+ {"/example-schema:protocols[.='http']", "http"s},
+ {"/example-schema:protocols[.='ftp']", "ftp"s},
+ {"/example-schema:protocols[.='pop3']", "pop3"s},
+ };
+ REQUIRE(datastore.getItems("/example-schema:protocols") == expected);
+ }
+
+ std::string sourcePath;
+ SECTION("begin")
+ {
+ REQUIRE_CALL(mock, write("/example-schema:protocols", std::nullopt, "pop3"s));
+ sourcePath = "/example-schema:protocols[.='pop3']";
+ datastore.moveItem(sourcePath, yang::move::Absolute::Begin);
+ datastore.commitChanges();
+ expected = {
+ {"/example-schema:protocols", special_{SpecialValue::LeafList}},
+ {"/example-schema:protocols[.='pop3']", "pop3"s},
+ {"/example-schema:protocols[.='http']", "http"s},
+ {"/example-schema:protocols[.='ftp']", "ftp"s},
+ };
+ REQUIRE(datastore.getItems("/example-schema:protocols") == expected);
+ }
+
+ SECTION("end")
+ {
+ sourcePath = "/example-schema:protocols[.='http']";
+ REQUIRE_CALL(mock, write("/example-schema:protocols", "pop3"s, "http"s));
+ datastore.moveItem(sourcePath, yang::move::Absolute::End);
+ datastore.commitChanges();
+ expected = {
+ {"/example-schema:protocols", special_{SpecialValue::LeafList}},
+ {"/example-schema:protocols[.='ftp']", "ftp"s},
+ {"/example-schema:protocols[.='pop3']", "pop3"s},
+ {"/example-schema:protocols[.='http']", "http"s},
+ };
+ REQUIRE(datastore.getItems("/example-schema:protocols") == expected);
+ }
+
+ SECTION("after")
+ {
+ sourcePath = "/example-schema:protocols[.='http']";
+ REQUIRE_CALL(mock, write("/example-schema:protocols", "ftp"s, "http"s));
+ datastore.moveItem(sourcePath, yang::move::Relative{yang::move::Relative::Position::After, {{".", "ftp"s}}});
+ datastore.commitChanges();
+ expected = {
+ {"/example-schema:protocols", special_{SpecialValue::LeafList}},
+ {"/example-schema:protocols[.='ftp']", "ftp"s},
+ {"/example-schema:protocols[.='http']", "http"s},
+ {"/example-schema:protocols[.='pop3']", "pop3"s},
+ };
+ REQUIRE(datastore.getItems("/example-schema:protocols") == expected);
+ }
+
+ SECTION("before")
+ {
+ sourcePath = "/example-schema:protocols[.='http']";
+ REQUIRE_CALL(mock, write("/example-schema:protocols", "ftp"s, "http"s));
+ datastore.moveItem(sourcePath, yang::move::Relative{yang::move::Relative::Position::Before, {{".", "pop3"s}}});
+ datastore.commitChanges();
+ expected = {
+ {"/example-schema:protocols", special_{SpecialValue::LeafList}},
+ {"/example-schema:protocols[.='ftp']", "ftp"s},
+ {"/example-schema:protocols[.='http']", "http"s},
+ {"/example-schema:protocols[.='pop3']", "pop3"s},
+ };
+ REQUIRE(datastore.getItems("/example-schema:protocols") == expected);
+ }
+ }
+
+ SECTION("moving list instances")
+ {
+ DatastoreAccess::Tree expected;
+ {
+ // sysrepo does this twice for some reason, it's possibly a bug
+ REQUIRE_CALL(mock, write("/example-schema:players[name='John']", std::nullopt, ""s)).TIMES(2);
+ REQUIRE_CALL(mock, write("/example-schema:players[name='John']/name", std::nullopt, "John"s));
+ REQUIRE_CALL(mock, write("/example-schema:players[name='Eve']", std::nullopt, ""s));
+ REQUIRE_CALL(mock, write("/example-schema:players[name='Eve']", ""s, ""s));
+ REQUIRE_CALL(mock, write("/example-schema:players[name='Eve']/name", std::nullopt, "Eve"s));
+ REQUIRE_CALL(mock, write("/example-schema:players[name='Adam']", std::nullopt, ""s));
+ REQUIRE_CALL(mock, write("/example-schema:players[name='Adam']/name", std::nullopt, "Adam"s));
+ REQUIRE_CALL(mock, write("/example-schema:players[name='Adam']", ""s, ""s));
+ datastore.createListInstance("/example-schema:players[name='John']");
+ datastore.createListInstance("/example-schema:players[name='Eve']");
+ datastore.createListInstance("/example-schema:players[name='Adam']");
+ datastore.commitChanges();
+ expected = {
+ {"/example-schema:players[name='John']", special_{SpecialValue::List}},
+ {"/example-schema:players[name='John']/name", "John"s},
+ {"/example-schema:players[name='Eve']", special_{SpecialValue::List}},
+ {"/example-schema:players[name='Eve']/name", "Eve"s},
+ {"/example-schema:players[name='Adam']", special_{SpecialValue::List}},
+ {"/example-schema:players[name='Adam']/name", "Adam"s},
+ };
+ REQUIRE(datastore.getItems("/example-schema:players") == expected);
+ }
+
+ std::string sourcePath;
+ SECTION("begin")
+ {
+ sourcePath = "/example-schema:players[name='Adam']";
+ REQUIRE_CALL(mock, write("/example-schema:players[name='Adam']", std::nullopt, ""s));
+ datastore.moveItem(sourcePath, yang::move::Absolute::Begin);
+ datastore.commitChanges();
+ expected = {
+ {"/example-schema:players[name='Adam']", special_{SpecialValue::List}},
+ {"/example-schema:players[name='Adam']/name", "Adam"s},
+ {"/example-schema:players[name='John']", special_{SpecialValue::List}},
+ {"/example-schema:players[name='John']/name", "John"s},
+ {"/example-schema:players[name='Eve']", special_{SpecialValue::List}},
+ {"/example-schema:players[name='Eve']/name", "Eve"s},
+ };
+ REQUIRE(datastore.getItems("/example-schema:players") == expected);
+ }
+
+ SECTION("end")
+ {
+ sourcePath = "/example-schema:players[name='John']";
+ REQUIRE_CALL(mock, write("/example-schema:players[name='John']", ""s, ""s));
+ datastore.moveItem(sourcePath, yang::move::Absolute::End);
+ datastore.commitChanges();
+ expected = {
+ {"/example-schema:players[name='Eve']", special_{SpecialValue::List}},
+ {"/example-schema:players[name='Eve']/name", "Eve"s},
+ {"/example-schema:players[name='Adam']", special_{SpecialValue::List}},
+ {"/example-schema:players[name='Adam']/name", "Adam"s},
+ {"/example-schema:players[name='John']", special_{SpecialValue::List}},
+ {"/example-schema:players[name='John']/name", "John"s},
+ };
+ REQUIRE(datastore.getItems("/example-schema:players") == expected);
+ }
+
+ SECTION("after")
+ {
+ sourcePath = "/example-schema:players[name='John']";
+ REQUIRE_CALL(mock, write("/example-schema:players[name='John']", ""s, ""s));
+ datastore.moveItem(sourcePath, yang::move::Relative{yang::move::Relative::Position::After, {{"name", "Eve"s}}});
+ datastore.commitChanges();
+ expected = {
+ {"/example-schema:players[name='Eve']", special_{SpecialValue::List}},
+ {"/example-schema:players[name='Eve']/name", "Eve"s},
+ {"/example-schema:players[name='John']", special_{SpecialValue::List}},
+ {"/example-schema:players[name='John']/name", "John"s},
+ {"/example-schema:players[name='Adam']", special_{SpecialValue::List}},
+ {"/example-schema:players[name='Adam']/name", "Adam"s},
+ };
+ REQUIRE(datastore.getItems("/example-schema:players") == expected);
+ }
+
+ SECTION("before")
+ {
+ sourcePath = "/example-schema:players[name='John']";
+ REQUIRE_CALL(mock, write("/example-schema:players[name='John']", ""s, ""s));
+ datastore.moveItem(sourcePath, yang::move::Relative{yang::move::Relative::Position::Before, {{"name", "Adam"s}}});
+ datastore.commitChanges();
+ expected = {
+ {"/example-schema:players[name='Eve']", special_{SpecialValue::List}},
+ {"/example-schema:players[name='Eve']/name", "Eve"s},
+ {"/example-schema:players[name='John']", special_{SpecialValue::List}},
+ {"/example-schema:players[name='John']/name", "John"s},
+ {"/example-schema:players[name='Adam']", special_{SpecialValue::List}},
+ {"/example-schema:players[name='Adam']/name", "Adam"s},
+ };
+ REQUIRE(datastore.getItems("/example-schema:players") == expected);
+ }
+ }
+
waitForCompletionAndBitMore(seq1);
}
diff --git a/tests/datastoreaccess_mock.hpp b/tests/datastoreaccess_mock.hpp
index ac75875..354dd44 100644
--- a/tests/datastoreaccess_mock.hpp
+++ b/tests/datastoreaccess_mock.hpp
@@ -27,6 +27,7 @@
IMPLEMENT_MOCK1(deleteLeafListInstance);
IMPLEMENT_MOCK1(createListInstance);
IMPLEMENT_MOCK1(deleteListInstance);
+ IMPLEMENT_MOCK2(moveItem);
IMPLEMENT_MOCK2(executeRpc);
// Can't use IMPLEMENT_MOCK for private methods - IMPLEMENT_MOCK needs full visibility of the method
diff --git a/tests/example-schema.yang b/tests/example-schema.yang
index 3f32a87..fdd8daf 100644
--- a/tests/example-schema.yang
+++ b/tests/example-schema.yang
@@ -251,4 +251,17 @@
leaf-list addresses {
type string;
}
+
+ leaf-list protocols {
+ type string;
+ ordered-by user;
+ }
+
+ list players {
+ key "name";
+ ordered-by user;
+ leaf name {
+ type string;
+ }
+ }
}
diff --git a/tests/list_manipulation.cpp b/tests/list_manipulation.cpp
index 35cd019..c646e13 100644
--- a/tests/list_manipulation.cpp
+++ b/tests/list_manipulation.cpp
@@ -26,6 +26,7 @@
schema->addLeaf("/mod:company", "mod:department", schema->validIdentities("other", "deptypes"));
schema->addList("/mod:company", "mod:inventory", {"id"});
schema->addLeaf("/mod:company/mod:inventory", "mod:id", yang::Int32{});
+ schema->addContainer("/", "mod:cont");
Parser parser(schema);
std::string input;
std::ostringstream errorStream;
@@ -83,4 +84,89 @@
REQUIRE(commandGet.type() == typeid(get_));
REQUIRE(boost::get<get_>(commandGet) == expectedGet);
}
+
+ SECTION("moving (leaf)list instances")
+ {
+ move_ expected;
+ SECTION("begin")
+ {
+ SECTION("cwd: /")
+ {
+ input = "move mod:addresses['1.2.3.4'] begin";
+ expected.m_source.m_nodes.push_back(dataNode_{module_{"mod"}, leafListElement_{"addresses", "1.2.3.4"s}});
+ }
+
+ SECTION("cwd: /mod:cont")
+ {
+ parser.changeNode(dataPath_{Scope::Absolute, {dataNode_{module_{"mod"}, container_{"cont"}}}});
+ SECTION("relative")
+ {
+ input = "move ../mod:addresses['1.2.3.4'] begin";
+ expected.m_source.m_nodes.push_back(dataNode_{nodeup_{}});
+ expected.m_source.m_nodes.push_back(dataNode_{module_{"mod"}, leafListElement_{"addresses", "1.2.3.4"s}});
+ }
+
+ SECTION("absolute")
+ {
+ input = "move /mod:addresses['1.2.3.4'] begin";
+ expected.m_source.m_scope = Scope::Absolute;
+ expected.m_source.m_nodes.push_back(dataNode_{module_{"mod"}, leafListElement_{"addresses", "1.2.3.4"s}});
+ }
+ }
+
+ expected.m_destination = yang::move::Absolute::Begin;
+ }
+
+ SECTION("end")
+ {
+ input = "move mod:addresses['1.2.3.4'] end";
+ expected.m_source.m_nodes.push_back(dataNode_{module_{"mod"}, leafListElement_{"addresses", "1.2.3.4"s}});
+ expected.m_destination = yang::move::Absolute::End;
+ }
+
+ SECTION("after")
+ {
+ input = "move mod:addresses['1.2.3.4'] after '0.0.0.0'";
+ expected.m_source.m_nodes.push_back(dataNode_{module_{"mod"}, leafListElement_{"addresses", "1.2.3.4"s}});
+ expected.m_destination = yang::move::Relative {
+ yang::move::Relative::Position::After,
+ {{".", "0.0.0.0"s}}
+ };
+ }
+
+ SECTION("before")
+ {
+ input = "move mod:addresses['1.2.3.4'] before '0.0.0.0'";
+ expected.m_source.m_nodes.push_back(dataNode_{module_{"mod"}, leafListElement_{"addresses", "1.2.3.4"s}});
+ expected.m_destination = yang::move::Relative {
+ yang::move::Relative::Position::Before,
+ {{".", "0.0.0.0"s}}
+ };
+ }
+
+ SECTION("list instance with destination")
+ {
+ input = "move mod:list[number=12] before [number=15]";
+ auto keys = std::map<std::string, leaf_data_>{
+ {"number", int32_t{12}}};
+ expected.m_source.m_nodes.push_back(dataNode_{module_{"mod"}, listElement_("list", keys)});
+ expected.m_destination = yang::move::Relative {
+ yang::move::Relative::Position::Before,
+ ListInstance{{"number", int32_t{15}}}
+ };
+ }
+
+ SECTION("list instance without destination")
+ {
+ input = "move mod:list[number=3] begin";
+ auto keys = std::map<std::string, leaf_data_>{
+ {"number", int32_t{3}}};
+ expected.m_source.m_nodes.push_back(dataNode_{module_{"mod"}, listElement_("list", keys)});
+ expected.m_destination = yang::move::Absolute::Begin;
+ }
+
+ command_ commandMove = parser.parseCommand(input, errorStream);
+ REQUIRE(commandMove.type() == typeid(move_));
+ REQUIRE(boost::get<move_>(commandMove) == expected);
+ }
}
diff --git a/tests/pretty_printers.hpp b/tests/pretty_printers.hpp
index 429a0e9..69c06f5 100644
--- a/tests/pretty_printers.hpp
+++ b/tests/pretty_printers.hpp
@@ -26,7 +26,7 @@
return s;
}
-std::ostream& operator<<(std::ostream& s, const DatastoreAccess::Tree& map)
+std::ostream& operator<<(std::ostream& s, const ListInstance& map)
{
s << std::endl
<< "{";
@@ -37,6 +37,16 @@
return s;
}
+std::ostream& operator<<(std::ostream& s, const DatastoreAccess::Tree& tree)
+{
+ s << "DatastoreAccess::Tree {\n";
+ for (const auto& [xpath, value] : tree) {
+ s << " {" << xpath << ", " << leafDataToString(value) << "}\n";
+ }
+ s << "}\n";
+ return s;
+}
+
std::ostream& operator<<(std::ostream& s, const yang::LeafDataType& type)
{
s << std::endl
@@ -133,3 +143,30 @@
s << "\nls_ {\n " << ls.m_path << "}\n";
return s;
}
+
+std::ostream& operator<<(std::ostream& s, const move_& move)
+{
+ s << "\nmove_ {\n";
+ s << " path: " << move.m_source;
+ s << " mode: ";
+ if (std::holds_alternative<yang::move::Absolute>(move.m_destination)) {
+ if (std::get<yang::move::Absolute>(move.m_destination) == yang::move::Absolute::Begin) {
+ s << "Absolute::Begin";
+ } else {
+ s << "Absolute::End";
+ }
+ } else {
+ const yang::move::Relative& relative = std::get<yang::move::Relative>(move.m_destination);
+ s << "Relative {\n";
+ s << " position: ";
+ if (relative.m_position == yang::move::Relative::Position::After) {
+ s << "Position::After\n";
+ } else {
+ s << "Position::Before\n";
+ }
+ s << " path: ";
+ s << relative.m_path;
+ }
+ s << "\n}\n";
+ return s;
+}