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;
+}