Add copy command

Change-Id: I0a88f7fa9a096022dd95e8af8854f980ca34f043
diff --git a/src/ast_commands.hpp b/src/ast_commands.hpp
index ddaac35..f039ea5 100644
--- a/src/ast_commands.hpp
+++ b/src/ast_commands.hpp
@@ -163,8 +163,23 @@
     boost::variant<schemaPath_, dataPath_> m_path;
 };
 
+struct copy_ : x3::position_tagged {
+    static constexpr auto name = "copy";
+    static constexpr auto shortHelp = "copy - copy configuration datastores around";
+    static constexpr auto longHelp = R"(
+    copy <source> <destination>
+
+    Usage:
+        /> copy running startup
+        /> copy startup running)";
+    bool operator==(const copy_& b) const;
+
+    Datastore m_source;
+    Datastore m_destination;
+};
+
 struct help_;
-using CommandTypes = boost::mpl::vector<cd_, commit_, create_, delete_, describe_, discard_, get_, help_, ls_, set_>;
+using CommandTypes = boost::mpl::vector<cd_, commit_, copy_, create_, delete_, describe_, discard_, get_, help_, ls_, set_>;
 struct help_ : x3::position_tagged {
     static constexpr auto name = "help";
     static constexpr auto shortHelp = "help - Print help for commands.";
@@ -209,3 +224,4 @@
 BOOST_FUSION_ADAPT_STRUCT(help_, m_cmd)
 BOOST_FUSION_ADAPT_STRUCT(discard_)
 BOOST_FUSION_ADAPT_STRUCT(get_, m_path)
+BOOST_FUSION_ADAPT_STRUCT(copy_, m_source, m_destination)
diff --git a/src/ast_handlers.hpp b/src/ast_handlers.hpp
index 667cb83..714657e 100644
--- a/src/ast_handlers.hpp
+++ b/src/ast_handlers.hpp
@@ -420,6 +420,8 @@
 
 struct get_class;
 
+struct copy_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)
diff --git a/src/ast_values.hpp b/src/ast_values.hpp
index b79ac84..f2d1c7e 100644
--- a/src/ast_values.hpp
+++ b/src/ast_values.hpp
@@ -54,6 +54,11 @@
     SpecialValue m_value;
 };
 
+enum class Datastore {
+    Running,
+    Startup
+};
+
 std::string specialValueToString(const special_& value);
 
 using leaf_data_ = boost::variant<enum_,
diff --git a/src/datastore_access.hpp b/src/datastore_access.hpp
index 6686b92..e70d9c9 100644
--- a/src/datastore_access.hpp
+++ b/src/datastore_access.hpp
@@ -54,6 +54,7 @@
 
     virtual void commitChanges() = 0;
     virtual void discardChanges() = 0;
+    virtual void copyConfig(const Datastore source, const Datastore destination) = 0;
 
 private:
     friend class DataQuery;
diff --git a/src/grammars.hpp b/src/grammars.hpp
index 9e4e35f..66fcfb3 100644
--- a/src/grammars.hpp
+++ b/src/grammars.hpp
@@ -36,7 +36,7 @@
 x3::rule<leaf_path_class, dataPath_> const leafPath = "leafPath";
 x3::rule<presenceContainerPath_class, dataPath_> const presenceContainerPath = "presenceContainerPath";
 x3::rule<listInstancePath_class, dataPath_> const listInstancePath = "listInstancePath";
-x3::rule<space_separator_class, x3::unused_type> const space_separator = "space_separator";
+x3::rule<space_separator_class, x3::unused_type> const space_separator = "a space";
 
 x3::rule<discard_class, discard_> const discard = "discard";
 x3::rule<ls_class, ls_> const ls = "ls";
@@ -48,6 +48,7 @@
 x3::rule<commit_class, commit_> const commit = "commit";
 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<command_class, command_> const command = "command";
 
 x3::rule<initializePath_class, x3::unused_type> const initializePath = "initializePath";
@@ -217,6 +218,55 @@
 auto const help_def =
     help_::name > createCommandSuggestions >> -command_names;
 
+struct datastore_symbol_table : x3::symbols<Datastore> {
+    datastore_symbol_table()
+    {
+        add
+            ("running", Datastore::Running)
+            ("startup", Datastore::Startup);
+    }
+} const datastore;
+
+auto copy_source = x3::rule<class source, Datastore>{"source datastore"} = datastore;
+auto copy_destination = x3::rule<class source, Datastore>{"destination datastore"} = datastore;
+
+const auto datastoreSuggestions = x3::eps[([](auto& ctx) {
+    auto& parserContext = x3::get<parser_context_tag>(ctx);
+    parserContext.m_suggestions = {Completion{"running", " "}, Completion{"startup", " "}};
+    parserContext.m_completionIterator = _where(ctx).begin();
+})];
+
+struct copy_args : x3::parser<copy_args> {
+    using attribute_type = copy_;
+    template <typename It, typename Ctx, typename RCtx>
+    bool parse(It& begin, It end, Ctx const& ctx, RCtx& rctx, copy_& attr) const
+    {
+        auto& parserContext = x3::get<parser_context_tag>(ctx);
+        auto iterBeforeDestination = begin;
+        auto save_iter = x3::no_skip[x3::eps[([&iterBeforeDestination](auto& ctx) {iterBeforeDestination = _where(ctx).begin();})]];
+        auto grammar = datastoreSuggestions > copy_source > space_separator > datastoreSuggestions > save_iter > copy_destination;
+
+        try {
+            grammar.parse(begin, end, ctx, rctx, attr);
+        } catch (x3::expectation_failure<It>& ex) {
+            using namespace std::string_literals;
+            parserContext.m_errorMsg = "Expected "s + ex.which() + " here:";
+            throw;
+        }
+
+        if (attr.m_source == attr.m_destination) {
+            begin = iterBeforeDestination; // Restoring the iterator here makes the error caret point to the second datastore
+            parserContext.m_errorMsg = "Source datastore and destination datastore can't be the same.";
+            return false;
+        }
+
+        return true;
+    }
+} copy_args;
+
+auto const copy_def =
+    copy_::name > space_separator > copy_args;
+
 auto const describe_def =
     describe_::name >> space_separator > (dataPathListEnd | dataPath | schemaPath);
 
@@ -224,7 +274,7 @@
     x3::eps;
 
 auto const command_def =
-    createCommandSuggestions >> x3::expect[cd | 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];
 
 #if __clang__
 #pragma GCC diagnostic pop
@@ -263,6 +313,7 @@
 BOOST_SPIRIT_DEFINE(delete_rule)
 BOOST_SPIRIT_DEFINE(describe)
 BOOST_SPIRIT_DEFINE(help)
+BOOST_SPIRIT_DEFINE(copy)
 BOOST_SPIRIT_DEFINE(command)
 BOOST_SPIRIT_DEFINE(createPathSuggestions)
 BOOST_SPIRIT_DEFINE(createKeySuggestions)
diff --git a/src/interpreter.cpp b/src/interpreter.cpp
index ca227be..1cfef74 100644
--- a/src/interpreter.cpp
+++ b/src/interpreter.cpp
@@ -80,6 +80,11 @@
         std::cout << it << std::endl;
 }
 
+void Interpreter::operator()(const copy_& copy) const
+{
+    m_datastore.copyConfig(copy.m_source, copy.m_destination);
+}
+
 std::string Interpreter::buildTypeInfo(const std::string& path) const
 {
     std::ostringstream ss;
diff --git a/src/interpreter.hpp b/src/interpreter.hpp
index a05183f..0dfe2b6 100644
--- a/src/interpreter.hpp
+++ b/src/interpreter.hpp
@@ -25,6 +25,7 @@
     void operator()(const describe_&) const;
     void operator()(const discard_&) const;
     void operator()(const help_&) const;
+    void operator()(const copy_& copy) const;
 
 private:
     template <typename T>
diff --git a/src/netconf-client.cpp b/src/netconf-client.cpp
index 63045f0..d824324 100644
--- a/src/netconf-client.cpp
+++ b/src/netconf-client.cpp
@@ -381,6 +381,15 @@
     }
 }
 
+void Session::copyConfig(const NC_DATASTORE source, const NC_DATASTORE destination)
+{
+    auto rpc = impl::guarded(nc_rpc_copy(destination, nullptr, source, nullptr, NC_WD_UNKNOWN, NC_PARAMTYPE_CONST));
+    if (!rpc) {
+        throw std::runtime_error("Cannot create copy-config RPC");
+    }
+    impl::do_rpc_ok(this, std::move(rpc));
+}
+
 ReportedError::ReportedError(const std::string& what)
     : std::runtime_error(what)
 {
diff --git a/src/netconf-client.hpp b/src/netconf-client.hpp
index 143f200..2db21c9 100644
--- a/src/netconf-client.hpp
+++ b/src/netconf-client.hpp
@@ -45,6 +45,7 @@
                     const std::string& data);
     void copyConfigFromString(const NC_DATASTORE target, const std::string& data);
     std::shared_ptr<libyang::Data_Node> rpc(const std::string& xmlData);
+    void copyConfig(const NC_DATASTORE source, const NC_DATASTORE destination);
     void commit();
     void discard();
 
diff --git a/src/netconf_access.cpp b/src/netconf_access.cpp
index f744dc6..5fca200 100644
--- a/src/netconf_access.cpp
+++ b/src/netconf_access.cpp
@@ -149,6 +149,22 @@
     return res;
 }
 
+NC_DATASTORE toNcDatastore(Datastore datastore)
+{
+    switch (datastore) {
+    case Datastore::Running:
+        return NC_DATASTORE_RUNNING;
+    case Datastore::Startup:
+        return NC_DATASTORE_STARTUP;
+    }
+    __builtin_unreachable();
+}
+
+void NetconfAccess::copyConfig(const Datastore source, const Datastore destination)
+{
+    m_session->copyConfig(toNcDatastore(source), toNcDatastore(destination));
+}
+
 std::string NetconfAccess::fetchSchema(const std::string_view module, const
         std::optional<std::string_view> revision, const
         std::optional<std::string_view> submodule, const
diff --git a/src/netconf_access.hpp b/src/netconf_access.hpp
index aa13312..107fcd3 100644
--- a/src/netconf_access.hpp
+++ b/src/netconf_access.hpp
@@ -42,6 +42,7 @@
     void commitChanges() override;
     void discardChanges() override;
     Tree executeRpc(const std::string& path, const Tree& input) override;
+    void copyConfig(const Datastore source, const Datastore destination) override;
 
     std::shared_ptr<Schema> schema() override;
 
diff --git a/src/sysrepo_access.cpp b/src/sysrepo_access.cpp
index b681893..2d1a1b4 100644
--- a/src/sysrepo_access.cpp
+++ b/src/sysrepo_access.cpp
@@ -267,6 +267,25 @@
     return res;
 }
 
+sr_datastore_t toSrDatastore(Datastore datastore)
+{
+    switch (datastore) {
+    case Datastore::Running:
+        return SR_DS_RUNNING;
+    case Datastore::Startup:
+        return SR_DS_STARTUP;
+    }
+    __builtin_unreachable();
+}
+
+void SysrepoAccess::copyConfig(const Datastore source, const Datastore destination)
+{
+    m_session->copy_config(nullptr, toSrDatastore(source), toSrDatastore(destination));
+    if (destination == Datastore::Running) {
+        m_session->refresh();
+    }
+}
+
 std::string SysrepoAccess::fetchSchema(const char* module, const char* revision, const char* submodule)
 {
     std::string schema;
diff --git a/src/sysrepo_access.hpp b/src/sysrepo_access.hpp
index cebb564..da5fb75 100644
--- a/src/sysrepo_access.hpp
+++ b/src/sysrepo_access.hpp
@@ -40,6 +40,7 @@
 
     void commitChanges() override;
     void discardChanges() override;
+    void copyConfig(const Datastore source, const Datastore destination) override;
 
 private:
     std::vector<std::map<std::string, leaf_data_>> listInstances(const std::string& path) override;
diff --git a/tests/command_completion.cpp b/tests/command_completion.cpp
index eb4949a..607b8ab 100644
--- a/tests/command_completion.cpp
+++ b/tests/command_completion.cpp
@@ -22,21 +22,21 @@
     SECTION("")
     {
         input = "";
-        expectedCompletions = {"cd", "create", "delete", "set", "commit", "get", "ls", "discard", "help", "describe"};
+        expectedCompletions = {"cd", "copy", "create", "delete", "set", "commit", "get", "ls", "discard", "help", "describe"};
         expectedContextLength = 0;
     }
 
     SECTION(" ")
     {
         input = " ";
-        expectedCompletions = {"cd", "create", "delete", "set", "commit", "get", "ls", "discard", "help", "describe"};
+        expectedCompletions = {"cd", "copy", "create", "delete", "set", "commit", "get", "ls", "discard", "help", "describe"};
         expectedContextLength = 0;
     }
 
     SECTION("c")
     {
         input = "c";
-        expectedCompletions = {"cd", "commit", "create"};
+        expectedCompletions = {"cd", "commit", "copy", "create"};
         expectedContextLength = 1;
     }
 
@@ -68,5 +68,12 @@
         expectedContextLength = 6;
     }
 
+    SECTION("copy datastores")
+    {
+        input = "copy ";
+        expectedCompletions = {"running", "startup"};
+        expectedContextLength = 0;
+    }
+
     REQUIRE(parser.completeCommand(input, errorStream) == (Completions{expectedCompletions, expectedContextLength}));
 }
diff --git a/tests/datastore_access.cpp b/tests/datastore_access.cpp
index a6cadc3..a1f9322 100644
--- a/tests/datastore_access.cpp
+++ b/tests/datastore_access.cpp
@@ -336,6 +336,20 @@
     }
 
 
+    SECTION("copying data from startup refreshes the data")
+    {
+        {
+            REQUIRE(datastore.getItems("/example-schema:leafInt16") == DatastoreAccess::Tree{});
+            REQUIRE_CALL(mock, write("/example-schema:leafInt16", std::nullopt, "123"s));
+            datastore.setLeaf("/example-schema:leafInt16", int16_t{123});
+            datastore.commitChanges();
+        }
+        REQUIRE(datastore.getItems("/example-schema:leafInt16") == DatastoreAccess::Tree{{"/example-schema:leafInt16", int16_t{123}}});
+        REQUIRE_CALL(mock, write("/example-schema:leafInt16", "123"s, std::nullopt));
+        datastore.copyConfig(Datastore::Startup, Datastore::Running);
+        REQUIRE(datastore.getItems("/example-schema:leafInt16") == DatastoreAccess::Tree{});
+    }
+
     waitForCompletionAndBitMore(seq1);
 }
 
diff --git a/tests/datastoreaccess_mock.hpp b/tests/datastoreaccess_mock.hpp
index e9dc870..8798973 100644
--- a/tests/datastoreaccess_mock.hpp
+++ b/tests/datastoreaccess_mock.hpp
@@ -35,6 +35,7 @@
 
     IMPLEMENT_MOCK0(commitChanges);
     IMPLEMENT_MOCK0(discardChanges);
+    IMPLEMENT_MOCK2(copyConfig);
 };