Add move command for moving (leaf)list instances

Change-Id: I0bff25209f74601a450c12a810200b3c124d65f2
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;
+}