Add parser support for YANG actions

Change-Id: I9e96ae15cee3f990dfa5ec3fac433faf71348e11
diff --git a/src/ast_commands.hpp b/src/ast_commands.hpp
index a594228..b863640 100644
--- a/src/ast_commands.hpp
+++ b/src/ast_commands.hpp
@@ -219,17 +219,17 @@
     DataFormat m_format;
 };
 
-struct rpc_ : x3::position_tagged {
-    static constexpr auto name = "rpc";
-    static constexpr auto shortHelp = "rpc - initiate RPC";
+struct prepare_ : x3::position_tagged {
+    static constexpr auto name = "prepare";
+    static constexpr auto shortHelp = "prepare - initiate RPC or action";
     static constexpr auto longHelp = R"(
-    rpc <rpc-path>
+    prepare <path-to-rpc-or-action>
 
     This command puts you into a mode where you can set your input parameters.
 
     Usage:
-        /> rpc <path-to-rpc>)";
-    bool operator==(const rpc_& other) const;
+        /> prepare <path-to-rpc-or-action>)";
+    bool operator==(const prepare_& other) const;
 
     dataPath_ m_path;
 };
@@ -261,7 +261,7 @@
 };
 
 struct help_;
-using CommandTypes = boost::mpl::vector<cancel_, cd_, commit_, copy_, create_, delete_, describe_, discard_, dump_, exec_, get_, help_, ls_, move_, rpc_, set_>;
+using CommandTypes = boost::mpl::vector<cancel_, cd_, commit_, copy_, create_, delete_, describe_, discard_, dump_, exec_, get_, help_, ls_, move_, prepare_, set_>;
 struct help_ : x3::position_tagged {
     static constexpr auto name = "help";
     static constexpr auto shortHelp = "help - Print help for commands.";
@@ -309,4 +309,4 @@
 BOOST_FUSION_ADAPT_STRUCT(copy_, m_source, m_destination)
 BOOST_FUSION_ADAPT_STRUCT(move_, m_source, m_destination)
 BOOST_FUSION_ADAPT_STRUCT(dump_, m_format)
-BOOST_FUSION_ADAPT_STRUCT(rpc_, m_path)
+BOOST_FUSION_ADAPT_STRUCT(prepare_, m_path)
diff --git a/src/ast_handlers.hpp b/src/ast_handlers.hpp
index 87cacd1..e583207 100644
--- a/src/ast_handlers.hpp
+++ b/src/ast_handlers.hpp
@@ -232,6 +232,8 @@
 
 struct rpcPath_class;
 
+struct actionPath_class;
+
 struct cdPath_class;
 
 struct set_class {
@@ -260,7 +262,9 @@
 
 struct dump_class;
 
-struct rpc_class;
+struct prepare_class;
+
+struct action_class;
 
 struct exec_class;
 
diff --git a/src/ast_path.cpp b/src/ast_path.cpp
index e8bfbcf..b9f7ae4 100644
--- a/src/ast_path.cpp
+++ b/src/ast_path.cpp
@@ -115,6 +115,11 @@
     return this->m_name == other.m_name;
 }
 
+bool actionNode_::operator==(const actionNode_& other) const
+{
+    return this->m_name == other.m_name;
+}
+
 list_::list_(const std::string& listName)
     : m_name(listName)
 {
diff --git a/src/ast_path.hpp b/src/ast_path.hpp
index d02be60..8a86181 100644
--- a/src/ast_path.hpp
+++ b/src/ast_path.hpp
@@ -87,9 +87,15 @@
     std::string m_name;
 };
 
+struct actionNode_ {
+    bool operator==(const actionNode_& other) const;
+
+    std::string m_name;
+};
+
 struct schemaNode_ {
     boost::optional<module_> m_prefix;
-    std::variant<container_, list_, nodeup_, leaf_, leafList_, rpcNode_> m_suffix;
+    std::variant<container_, list_, nodeup_, leaf_, leafList_, rpcNode_, actionNode_> m_suffix;
 
     schemaNode_();
     schemaNode_(decltype(m_suffix) node);
@@ -99,7 +105,7 @@
 
 struct dataNode_ {
     boost::optional<module_> m_prefix;
-    std::variant<container_, listElement_, nodeup_, leaf_, leafListElement_, leafList_, list_, rpcNode_> m_suffix;
+    std::variant<container_, listElement_, nodeup_, leaf_, leafListElement_, leafList_, list_, rpcNode_, actionNode_> m_suffix;
 
     dataNode_();
     dataNode_(decltype(m_suffix) node);
diff --git a/src/grammars.hpp b/src/grammars.hpp
index ee808d5..86dda33 100644
--- a/src/grammars.hpp
+++ b/src/grammars.hpp
@@ -29,7 +29,7 @@
 x3::rule<copy_class, copy_> const copy = "copy";
 x3::rule<move_class, move_> const move = "move";
 x3::rule<dump_class, dump_> const dump = "dump";
-x3::rule<rpc_class, rpc_> const rpc = "rpc";
+x3::rule<prepare_class, prepare_> const prepare = "prepare";
 x3::rule<exec_class, exec_> const exec = "exec";
 x3::rule<cancel_class, cancel_> const cancel = "cancel";
 x3::rule<command_class, command_> const command = "command";
@@ -258,8 +258,8 @@
     }
 } const dump_args;
 
-auto const rpc_def =
-    rpc_::name > space_separator > rpcPath;
+auto const prepare_def =
+    prepare_::name > space_separator > rpcActionPath;
 
 auto const exec_def =
     exec_::name >> x3::attr(exec_{});
@@ -274,7 +274,7 @@
     x3::eps;
 
 auto const command_def =
-    createCommandSuggestions >> x3::expect[cd | copy | create | delete_rule | set | commit | get | ls | discard | describe | help | move | dump | rpc | exec | cancel];
+    createCommandSuggestions >> x3::expect[cd | copy | create | delete_rule | set | commit | get | ls | discard | describe | help | move | dump | prepare | exec | cancel];
 
 #if __clang__
 #pragma GCC diagnostic pop
@@ -293,7 +293,7 @@
 BOOST_SPIRIT_DEFINE(copy)
 BOOST_SPIRIT_DEFINE(move)
 BOOST_SPIRIT_DEFINE(dump)
-BOOST_SPIRIT_DEFINE(rpc)
+BOOST_SPIRIT_DEFINE(prepare)
 BOOST_SPIRIT_DEFINE(exec)
 BOOST_SPIRIT_DEFINE(cancel)
 BOOST_SPIRIT_DEFINE(command)
diff --git a/src/interpreter.cpp b/src/interpreter.cpp
index 26e2be4..a39167e 100644
--- a/src/interpreter.cpp
+++ b/src/interpreter.cpp
@@ -210,24 +210,28 @@
     std::cout << m_datastore.dump(dump.m_format) << "\n";
 }
 
-void Interpreter::operator()(const rpc_& rpc) const
+void Interpreter::operator()(const prepare_& prepare) const
 {
-    m_datastore.initiateRpc(pathToString(toCanonicalPath(rpc.m_path)));
-    m_parser.changeNode(rpc.m_path);
+    if (std::holds_alternative<rpcNode_>(prepare.m_path.m_nodes.back().m_suffix)) {
+        m_datastore.initiateRpc(pathToString(toCanonicalPath(prepare.m_path)));
+    } else {
+        m_datastore.initiateAction(pathToString(toCanonicalPath(prepare.m_path)));
+    }
+    m_parser.changeNode(prepare.m_path);
 }
 
 void Interpreter::operator()(const exec_&) const
 {
     m_parser.changeNode({Scope::Absolute, {}});
-    auto output = m_datastore.executeRpc();
-    std::cout << "RPC output:\n";
+    auto output = m_datastore.execute();
+    std::cout << "RPC/action output:\n";
     printTree(output);
 }
 
 void Interpreter::operator()(const cancel_&) const
 {
     m_parser.changeNode({Scope::Absolute, {}});
-    m_datastore.cancelRpc();
+    m_datastore.cancel();
 }
 
 struct commandLongHelpVisitor : boost::static_visitor<const char*> {
diff --git a/src/interpreter.hpp b/src/interpreter.hpp
index 0807c02..54a8d05 100644
--- a/src/interpreter.hpp
+++ b/src/interpreter.hpp
@@ -29,7 +29,7 @@
     void operator()(const copy_& copy) const;
     void operator()(const move_& move) const;
     void operator()(const dump_& dump) const;
-    void operator()(const rpc_& rpc) const;
+    void operator()(const prepare_& prepare) const;
     void operator()(const exec_& exec) const;
     void operator()(const cancel_& cancel) const;
 
diff --git a/src/path_parser.hpp b/src/path_parser.hpp
index 00629bb..65e5140 100644
--- a/src/path_parser.hpp
+++ b/src/path_parser.hpp
@@ -144,6 +144,9 @@
                     parserContext.m_suggestions.emplace(Completion{parseString + "/"});
                     break;
                 case yang::NodeTypes::Action:
+                    out.m_suffix = actionNode_{child.second};
+                    parserContext.m_suggestions.emplace(Completion{parseString + "/"});
+                    break;
                 case yang::NodeTypes::AnyXml:
                 case yang::NodeTypes::Notification:
                     continue;
@@ -423,22 +426,36 @@
 
 } writableLeafPath;
 
-auto const writableLeafPath_def =
-    PathParser<PathParserMode::DataPath, CompletionMode::Data>{filterConfigFalse};
+struct RpcActionPath : x3::parser<RpcActionPath> {
+    using attribute_type = dataPath_;
+    template <typename It, typename Ctx, typename RCtx, typename Attr>
+    static bool parse(It& begin, It end, Ctx const& ctx, RCtx& rctx, Attr& attr)
+    {
+        bool res = dataPath.parse(begin, end, ctx, rctx, attr);
+        if (!res) {
+            return false;
+        }
 
-auto const onlyRpc = [] (const Schema& schema, const std::string& path) {
-    return schema.nodeType(path) == yang::NodeTypes::Rpc;
+        if (attr.m_nodes.empty()
+                || (!std::holds_alternative<actionNode_>(attr.m_nodes.back().m_suffix) && !std::holds_alternative<actionNode_>(attr.m_nodes.back().m_suffix))) {
+            auto& parserContext = x3::get<parser_context_tag>(ctx);
+            parserContext.m_errorMsg = "This is not a path to an RPC/action.";
+            return false;
+        }
+
+        return true;
+    }
 };
 
-auto const rpcPath_def =
-    PathParser<PathParserMode::DataPath, CompletionMode::Data>{onlyRpc};
+auto const rpcActionPath = as<dataPath_>[RpcActionPath()];
 
-auto const noRpc = [] (const Schema& schema, const std::string& path) {
-    return schema.nodeType(path) != yang::NodeTypes::Rpc;
+auto const noRpcOrAction = [] (const Schema& schema, const std::string& path) {
+    auto nodeType = schema.nodeType(path);
+    return nodeType != yang::NodeTypes::Rpc && nodeType != yang::NodeTypes::Action;
 };
 
 auto const cdPath_def =
-    PathParser<PathParserMode::DataPath, CompletionMode::Data>{noRpc};
+    PathParser<PathParserMode::DataPath, CompletionMode::Data>{noRpcOrAction};
 
 auto const presenceContainerPath_def =
     dataPath;
@@ -462,7 +479,6 @@
 BOOST_SPIRIT_DEFINE(keyValue)
 BOOST_SPIRIT_DEFINE(key_identifier)
 BOOST_SPIRIT_DEFINE(listSuffix)
-BOOST_SPIRIT_DEFINE(rpcPath)
 BOOST_SPIRIT_DEFINE(cdPath)
 BOOST_SPIRIT_DEFINE(presenceContainerPath)
 BOOST_SPIRIT_DEFINE(listInstancePath)
diff --git a/src/proxy_datastore.cpp b/src/proxy_datastore.cpp
index e6878cb..393edef 100644
--- a/src/proxy_datastore.cpp
+++ b/src/proxy_datastore.cpp
@@ -59,27 +59,52 @@
     return m_datastore->dump(format);
 }
 
+namespace {
+struct getInputPath
+{
+    template<typename InputType>
+    auto operator()(const InputType& input)
+    {
+        return input.m_path;
+    }
+};
+}
+
 void ProxyDatastore::initiateRpc(const std::string& rpcPath)
 {
     if (m_inputDatastore) {
-        throw std::runtime_error("RPC input already in progress (" + m_rpcPath + ")");
+        throw std::runtime_error("RPC/action input already in progress (" + std::visit(getInputPath{}, m_inputPath) + ")");
     }
     m_inputDatastore = m_createTemporaryDatastore(m_datastore);
-    m_rpcPath = rpcPath;
+    m_inputPath = RpcInput{rpcPath};
     m_inputDatastore->createItem(rpcPath);
 }
 
-DatastoreAccess::Tree ProxyDatastore::executeRpc()
+void ProxyDatastore::initiateAction(const std::string& actionPath)
+{
+    if (m_inputDatastore) {
+        throw std::runtime_error("RPC/action input already in progress (" + std::visit(getInputPath{}, m_inputPath) + ")");
+    }
+    m_inputDatastore = m_createTemporaryDatastore(m_datastore);
+    m_inputPath = ActionInput{actionPath};
+    m_inputDatastore->createItem(actionPath);
+}
+
+DatastoreAccess::Tree ProxyDatastore::execute()
 {
     if (!m_inputDatastore) {
-        throw std::runtime_error("No RPC input in progress");
+        throw std::runtime_error("No RPC/action input in progress");
     }
     auto inputData = m_inputDatastore->getItems("/");
     m_inputDatastore = nullptr;
-    return m_datastore->executeRpc(m_rpcPath, inputData);
+    if (std::holds_alternative<RpcInput>(m_inputPath)) {
+        return m_datastore->executeRpc(std::visit(getInputPath{}, m_inputPath), inputData);
+    } else {
+        return m_datastore->executeAction(std::visit(getInputPath{}, m_inputPath), inputData);
+    }
 }
 
-void ProxyDatastore::cancelRpc()
+void ProxyDatastore::cancel()
 {
     m_inputDatastore = nullptr;
 }
@@ -91,7 +116,7 @@
 
 std::shared_ptr<DatastoreAccess> ProxyDatastore::pickDatastore(const std::string& path) const
 {
-    if (!m_inputDatastore || !boost::starts_with(path, m_rpcPath)) {
+    if (!m_inputDatastore || !boost::starts_with(path, std::visit(getInputPath{}, m_inputPath))) {
         return m_datastore;
     } else {
         return m_inputDatastore;
diff --git a/src/proxy_datastore.hpp b/src/proxy_datastore.hpp
index c15a869..ab69193 100644
--- a/src/proxy_datastore.hpp
+++ b/src/proxy_datastore.hpp
@@ -30,8 +30,9 @@
     [[nodiscard]] std::string dump(const DataFormat format) const;
 
     void initiateRpc(const std::string& rpcPath);
-    [[nodiscard]] DatastoreAccess::Tree executeRpc();
-    void cancelRpc();
+    void initiateAction(const std::string& actionPath);
+    [[nodiscard]] DatastoreAccess::Tree execute();
+    void cancel();
 
     [[nodiscard]] std::shared_ptr<Schema> schema() const;
 private:
@@ -45,5 +46,15 @@
     std::shared_ptr<DatastoreAccess> m_datastore;
     std::function<std::shared_ptr<DatastoreAccess>(const std::shared_ptr<DatastoreAccess>&)> m_createTemporaryDatastore;
     std::shared_ptr<DatastoreAccess> m_inputDatastore;
-    std::string m_rpcPath;
+
+    struct RpcInput {
+        std::string m_path;
+    };
+
+    struct ActionInput {
+        std::string m_path;
+    };
+    // This variant is needed, so that I know whether to call executeRpc or executeAction
+    // TODO: get rid of this variant with sysrepo2 because the method for RPC/action is the same there
+    std::variant<ActionInput, RpcInput> m_inputPath;
 };
diff --git a/tests/command_completion.cpp b/tests/command_completion.cpp
index 49017b0..2fedb78 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", "move", "dump", "rpc", "exec", "cancel"};
+        expectedCompletions = {"cd", "copy", "create", "delete", "set", "commit", "get", "ls", "discard", "help", "describe", "move", "dump", "prepare", "exec", "cancel"};
         expectedContextLength = 0;
         SECTION("no space") {
             input = "";
diff --git a/tests/datastore_access.cpp b/tests/datastore_access.cpp
index 2fac222..da3bc48 100644
--- a/tests/datastore_access.cpp
+++ b/tests/datastore_access.cpp
@@ -992,7 +992,7 @@
             }
 
             catching<OnExec>([&] {REQUIRE(datastore->executeRpc(rpc, input) == output);});
-            catching<OnExec>([&] {REQUIRE(proxyDatastore.executeRpc() == output);});
+            catching<OnExec>([&] {REQUIRE(proxyDatastore.execute() == output);});
         }
 
         SECTION("non-existing RPC")
diff --git a/tests/interpreter.cpp b/tests/interpreter.cpp
index a88aa48..966855d 100644
--- a/tests/interpreter.cpp
+++ b/tests/interpreter.cpp
@@ -442,12 +442,12 @@
     {
         dataPath_ rpcPath;
         rpcPath.pushFragment({{"example"}, rpcNode_{"launch-nukes"}});
-        rpc_ rpcCmd;
-        rpcCmd.m_path = rpcPath;
+        prepare_ prepareCmd;
+        prepareCmd.m_path = rpcPath;
 
         {
             REQUIRE_CALL(*input_datastore, createItem("/example:launch-nukes"));
-            boost::apply_visitor(Interpreter(parser, proxyDatastore), command_{rpcCmd});
+            boost::apply_visitor(Interpreter(parser, proxyDatastore), command_{prepareCmd});
         }
 
         REQUIRE(parser.currentPath() == rpcPath);
diff --git a/tests/path_completion.cpp b/tests/path_completion.cpp
index 1c0c8f1..0b0d3f8 100644
--- a/tests/path_completion.cpp
+++ b/tests/path_completion.cpp
@@ -336,9 +336,9 @@
         expectedContextLength = 0;
     }
 
-    SECTION("rpc input nodes NOT completed for rpc command")
+    SECTION("rpc input nodes NOT completed for prepare command")
     {
-        input = "rpc example:fire/";
+        input = "prepare example:fire/";
         expectedCompletions = {};
         expectedContextLength = 13;
     }