Allow immediately executing RPC with no input

Story: https://tree.taiga.io/project/jktjkt-netconf-cli/us/189
Change-Id: I876437b798e995c4484e97b409117a2bb207e864
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 53dda3b..94cd1ee 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -347,7 +347,7 @@
     cli_test(path_utils)
     target_link_libraries(test_path_utils path)
     cli_test(keyvalue_completion)
-    cli_test(prepare)
+    cli_test(parser_rpc)
 
     configure_file(${CMAKE_CURRENT_SOURCE_DIR}/tests/init_datastore.bash.in
         ${CMAKE_CURRENT_BINARY_DIR}/init_datastore.bash @ONLY)
diff --git a/src/ast_commands.cpp b/src/ast_commands.cpp
index 5b99c6a..fa2b56c 100644
--- a/src/ast_commands.cpp
+++ b/src/ast_commands.cpp
@@ -51,3 +51,8 @@
 {
     return this->m_path == other.m_path;
 }
+
+bool exec_::operator==(const exec_& other) const
+{
+    return this->m_path == other.m_path;
+}
diff --git a/src/ast_commands.hpp b/src/ast_commands.hpp
index db78611..9ec2740 100644
--- a/src/ast_commands.hpp
+++ b/src/ast_commands.hpp
@@ -258,14 +258,20 @@
     static constexpr auto name = "exec";
     static constexpr auto shortHelp = "exec - Execute RPC/action.";
     static constexpr auto longHelp = R"(
-    exec
+    exec [path]
 
     This command executes the RPC/action you have previously initiated via the
     `prepare` command.
 
+    If the RPC/action has no input parameters, it can be directly execute via
+    `exec` without usgin `prepare`.
+
     Usage:
-        /> exec)";
+        /> exec
+        /> exec /mod:myRpc)";
     bool operator==(const exec_& other) const;
+
+    boost::optional<dataPath_> m_path;
 };
 
 struct cancel_ : x3::position_tagged {
@@ -332,3 +338,4 @@
 BOOST_FUSION_ADAPT_STRUCT(move_, m_source, m_destination)
 BOOST_FUSION_ADAPT_STRUCT(dump_, m_format)
 BOOST_FUSION_ADAPT_STRUCT(prepare_, m_path)
+BOOST_FUSION_ADAPT_STRUCT(exec_, m_path)
diff --git a/src/grammars.hpp b/src/grammars.hpp
index 02da179..2ff6e7a 100644
--- a/src/grammars.hpp
+++ b/src/grammars.hpp
@@ -259,10 +259,10 @@
 } const dump_args;
 
 auto const prepare_def =
-    prepare_::name > space_separator > rpcActionPath;
+    prepare_::name > space_separator > as<dataPath_>[RpcActionPath<AllowInput::Yes>{}];
 
 auto const exec_def =
-    exec_::name >> x3::attr(exec_{});
+    exec_::name > -(space_separator > as<dataPath_>[RpcActionPath<AllowInput::No>{}]);
 
 auto const cancel_def =
     cancel_::name >> x3::attr(cancel_{});
diff --git a/src/interpreter.cpp b/src/interpreter.cpp
index acb3f0b..ac958dc 100644
--- a/src/interpreter.cpp
+++ b/src/interpreter.cpp
@@ -220,9 +220,12 @@
     m_parser.changeNode(prepare.m_path);
 }
 
-void Interpreter::operator()(const exec_&) const
+void Interpreter::operator()(const exec_& exec) const
 {
     m_parser.changeNode({Scope::Absolute, {}});
+    if (exec.m_path) {
+        m_datastore.initiate(pathToString(toCanonicalPath(*exec.m_path)));
+    }
     auto output = m_datastore.execute();
     std::cout << "RPC/action output:\n";
     printTree(output);
diff --git a/src/path_parser.hpp b/src/path_parser.hpp
index 58d174e..c3df931 100644
--- a/src/path_parser.hpp
+++ b/src/path_parser.hpp
@@ -428,12 +428,28 @@
 
 } const writableLeafPath;
 
-struct RpcActionPath : x3::parser<RpcActionPath> {
+enum class AllowInput {
+    Yes,
+    No
+};
+
+template <AllowInput ALLOW_INPUT>
+struct RpcActionPath : x3::parser<RpcActionPath<ALLOW_INPUT>> {
     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);
+        auto grammar = PathParser<PathParserMode::DataPath, CompletionMode::Data>{[] (const Schema& schema, const std::string& path) {
+            if constexpr (ALLOW_INPUT == AllowInput::No) {
+                auto nodeType = schema.nodeType(path);
+                if (nodeType == yang::NodeTypes::Rpc || nodeType == yang::NodeTypes::Action) {
+                    return !schema.hasInputNodes(path);
+                }
+            }
+
+            return true;
+        }};
+        bool res = grammar.parse(begin, end, ctx, rctx, attr);
         if (!res) {
             return false;
         }
@@ -449,8 +465,6 @@
     }
 };
 
-auto const rpcActionPath = as<dataPath_>[RpcActionPath()];
-
 auto const noRpcOrAction = [](const Schema& schema, const std::string& path) {
     auto nodeType = schema.nodeType(path);
     return nodeType != yang::NodeTypes::Rpc && nodeType != yang::NodeTypes::Action;
diff --git a/src/schema.hpp b/src/schema.hpp
index 00586b5..6955686 100644
--- a/src/schema.hpp
+++ b/src/schema.hpp
@@ -68,6 +68,7 @@
     [[nodiscard]] virtual std::string leafrefPath(const std::string& leafrefPath) const = 0;
     [[nodiscard]] virtual std::optional<std::string> description(const std::string& location) const = 0;
     [[nodiscard]] virtual yang::Status status(const std::string& location) const = 0;
+    [[nodiscard]] virtual bool hasInputNodes(const std::string& path) const = 0;
 
     [[nodiscard]] virtual std::set<ModuleNodePair> availableNodes(const boost::variant<dataPath_, schemaPath_, module_>& path, const Recursion recursion) const = 0;
 };
diff --git a/src/static_schema.cpp b/src/static_schema.cpp
index a75f471..c1d4417 100644
--- a/src/static_schema.cpp
+++ b/src/static_schema.cpp
@@ -41,6 +41,8 @@
     //create a new set of children for the new node
     std::string key = joinPaths(location, name);
     m_nodes.emplace(key, std::unordered_map<std::string, NodeInfo>());
+    m_nodes.emplace(joinPaths(key, "input"), std::unordered_map<std::string, NodeInfo>());
+    m_nodes.emplace(joinPaths(key, "output"), std::unordered_map<std::string, NodeInfo>());
 }
 
 void StaticSchema::addAction(const std::string& location, const std::string& name)
@@ -272,6 +274,15 @@
     throw std::runtime_error{"Internal error: StaticSchema::status(std::string) not implemented. The tests should not have called this overload."};
 }
 
+bool StaticSchema::hasInputNodes(const std::string& path) const
+{
+    if (nodeType(path) != yang::NodeTypes::Action && nodeType(path) != yang::NodeTypes::Rpc) {
+        throw std::logic_error("StaticSchema::hasInputNodes called with non-RPC/action path");
+    }
+
+    return m_nodes.at(joinPaths(path, "input")).size() != 0;
+}
+
 yang::NodeTypes StaticSchema::nodeType(const std::string& path) const
 {
     auto locationString = stripLastNodeFromPath(path);
diff --git a/src/static_schema.hpp b/src/static_schema.hpp
index 3009d7e..0b9e59e 100644
--- a/src/static_schema.hpp
+++ b/src/static_schema.hpp
@@ -80,6 +80,7 @@
     std::set<ModuleNodePair> availableNodes(const boost::variant<dataPath_, schemaPath_, module_>& path, const Recursion recursion) const override;
     std::optional<std::string> description(const std::string& path) const override;
     yang::Status status(const std::string& location) const override;
+    bool hasInputNodes(const std::string& path) const override;
 
     /** A helper for making tests a little bit easier. It returns all
      * identities which are based on the argument passed and which can then be
diff --git a/src/yang_schema.cpp b/src/yang_schema.cpp
index 7f27711..d6c0bd8 100644
--- a/src/yang_schema.cpp
+++ b/src/yang_schema.cpp
@@ -485,6 +485,17 @@
     }
 }
 
+bool YangSchema::hasInputNodes(const std::string& path) const
+{
+    auto node = getSchemaNode(path.c_str());
+    if (auto type = node->nodetype(); type != LYS_ACTION && type != LYS_RPC) {
+        throw std::logic_error("StaticSchema::hasInputNodes called with non-RPC/action path");
+    }
+
+    // The first child gives the /input node and then I check whether it has a child.
+    return node->child()->child().get();
+}
+
 bool YangSchema::isConfig(const std::string& path) const
 {
     auto node = getSchemaNode(path.c_str());
diff --git a/src/yang_schema.hpp b/src/yang_schema.hpp
index 632f538..dc45097 100644
--- a/src/yang_schema.hpp
+++ b/src/yang_schema.hpp
@@ -47,6 +47,7 @@
     [[nodiscard]] std::set<ModuleNodePair> availableNodes(const boost::variant<dataPath_, schemaPath_, module_>& path, const Recursion recursion) const override;
     [[nodiscard]] std::optional<std::string> description(const std::string& path) const override;
     [[nodiscard]] yang::Status status(const std::string& location) const override;
+    [[nodiscard]] bool hasInputNodes(const std::string& path) const override;
 
     void registerModuleCallback(const std::function<std::string(const char*, const char*, const char*, const char*)>& clb);
 
diff --git a/tests/interpreter.cpp b/tests/interpreter.cpp
index c6d7c9c..4ef0a96 100644
--- a/tests/interpreter.cpp
+++ b/tests/interpreter.cpp
@@ -32,6 +32,7 @@
     MAKE_CONST_MOCK1(nodeType, yang::NodeTypes(const std::string&), override);
     MAKE_CONST_MOCK2(nodeType, yang::NodeTypes(const schemaPath_&, const ModuleNodePair&), override);
     IMPLEMENT_CONST_MOCK1(status);
+    IMPLEMENT_CONST_MOCK1(hasInputNodes);
 };
 
 TEST_CASE("interpreter tests")
diff --git a/tests/parser_rpc.cpp b/tests/parser_rpc.cpp
new file mode 100644
index 0000000..c8458d8
--- /dev/null
+++ b/tests/parser_rpc.cpp
@@ -0,0 +1,70 @@
+
+/*
+ * Copyright (C) 2018 CESNET, https://photonics.cesnet.cz/
+ * Copyright (C) 2018 FIT CVUT, https://fit.cvut.cz/
+ *
+ * Written by Václav Kubernát <kubervac@fit.cvut.cz>
+ *
+*/
+
+#include "trompeloeil_doctest.hpp"
+#include "parser.hpp"
+#include "pretty_printers.hpp"
+#include "static_schema.hpp"
+
+TEST_CASE("rpc")
+{
+    auto schema = std::make_shared<StaticSchema>();
+    schema->addModule("example");
+    schema->addRpc("/", "example:fire");
+    schema->addRpc("/", "example:shutdown");
+    schema->addLeaf("/example:shutdown/input", "example:interface", yang::String{});
+    schema->addList("/", "example:port", {"name"});
+    schema->addLeaf("/example:port", "example:name", yang::String{});
+    schema->addAction("/example:port", "example:shutdown");
+    Parser parser(schema);
+    std::string input;
+    std::ostringstream errorStream;
+    SECTION("with prepare")
+    {
+        prepare_ prepExpected;
+        prepExpected.m_path.m_scope = Scope::Relative;
+        SECTION("rpc")
+        {
+            input = "prepare example:fire";
+            prepExpected.m_path.m_nodes.emplace_back(module_{"example"}, rpcNode_{"fire"});
+        }
+
+        SECTION("action")
+        {
+            input = "prepare example:port[name='eth0']/shutdown";
+            prepExpected.m_path.m_nodes.emplace_back(module_{"example"}, listElement_{"port", {{"name", std::string{"eth0"}}}});
+            prepExpected.m_path.m_nodes.emplace_back(actionNode_{"shutdown"});
+        }
+
+        command_ command = parser.parseCommand(input, errorStream);
+        auto lol = boost::get<prepare_>(command);
+        REQUIRE(boost::get<prepare_>(command) == prepExpected);
+    }
+
+    SECTION("direct exec")
+    {
+        exec_ execExpected;
+        execExpected.m_path = dataPath_{};
+        execExpected.m_path->m_scope = Scope::Relative;
+        SECTION("the rpc has no arguments")
+        {
+            input = "exec example:fire";
+            execExpected.m_path->m_nodes.emplace_back(module_{"example"}, rpcNode_{"fire"});
+            command_ command = parser.parseCommand(input, errorStream);
+            auto lol = boost::get<exec_>(command);
+            REQUIRE(boost::get<exec_>(command) == execExpected);
+        }
+
+        SECTION("the rpc has arguments")
+        {
+            input = "exec example:shutdown";
+            REQUIRE_THROWS_AS(parser.parseCommand(input, errorStream), InvalidCommandException);
+        }
+    }
+}
diff --git a/tests/prepare.cpp b/tests/prepare.cpp
deleted file mode 100644
index d56a488..0000000
--- a/tests/prepare.cpp
+++ /dev/null
@@ -1,45 +0,0 @@
-
-/*
- * Copyright (C) 2018 CESNET, https://photonics.cesnet.cz/
- * Copyright (C) 2018 FIT CVUT, https://fit.cvut.cz/
- *
- * Written by Václav Kubernát <kubervac@fit.cvut.cz>
- *
-*/
-
-#include "trompeloeil_doctest.hpp"
-#include "parser.hpp"
-#include "pretty_printers.hpp"
-#include "static_schema.hpp"
-
-TEST_CASE("prepare command")
-{
-    auto schema = std::make_shared<StaticSchema>();
-    schema->addModule("example");
-    schema->addRpc("/", "example:fire");
-    schema->addList("/", "example:port", {"name"});
-    schema->addLeaf("/example:port", "example:name", yang::String{});
-    schema->addAction("/example:port", "example:shutdown");
-    Parser parser(schema);
-    std::string input;
-    std::ostringstream errorStream;
-    prepare_ expected;
-    expected.m_path.m_scope = Scope::Relative;
-    SECTION("rpc")
-    {
-        input = "prepare example:fire";
-        expected.m_path.m_nodes.emplace_back(module_{"example"}, rpcNode_{"fire"});
-    }
-
-    SECTION("action")
-    {
-        input = "prepare example:port[name='eth0']/shutdown";
-        expected.m_path.m_nodes.emplace_back(module_{"example"}, listElement_{"port", {{"name", std::string{"eth0"}}}});
-        expected.m_path.m_nodes.emplace_back(actionNode_{"shutdown"});
-    }
-
-    command_ command = parser.parseCommand(input, errorStream);
-    REQUIRE(command.type() == typeid(prepare_));
-    auto lol = boost::get<prepare_>(command);
-    REQUIRE(boost::get<prepare_>(command) == expected);
-}
diff --git a/tests/yang.cpp b/tests/yang.cpp
index 1ec6507..c5ae62a 100644
--- a/tests/yang.cpp
+++ b/tests/yang.cpp
@@ -317,6 +317,22 @@
 
     rpc myRpc {}
 
+    rpc rpcOneOutput {
+        output {
+            leaf ahoj {
+                type string;
+            }
+        }
+    }
+
+    rpc rpcOneInput {
+        input {
+            leaf ahoj {
+                type string;
+            }
+        }
+    }
+
     leaf numberOrString {
         type union {
             type int32;
@@ -776,6 +792,8 @@
                         {"example-schema"s, "obsoleteLeafWithDeprecatedType"},
                         {"example-schema"s, "obsoleteLeafWithObsoleteType"},
                         {"example-schema"s, "myRpc"},
+                        {"example-schema"s, "rpcOneOutput"},
+                        {"example-schema"s, "rpcOneInput"},
                         {"example-schema"s, "systemStats"},
                         {"example-schema"s, "dummyLeaf"},
                         {"example-schema"s, "addresses"},
@@ -855,6 +873,8 @@
                         {"example-schema"s, "length"},
                         {"example-schema"s, "loopback"},
                         {"example-schema"s, "myRpc"},
+                        {"example-schema"s, "rpcOneOutput"},
+                        {"example-schema"s, "rpcOneInput"},
                         {"example-schema"s, "numberOrString"},
                         {"example-schema"s, "obsoleteLeaf"},
                         {"example-schema"s, "obsoleteLeafWithDeprecatedType"},
@@ -920,6 +940,14 @@
                         {boost::none, "/example-schema:myRpc"},
                         {boost::none, "/example-schema:myRpc/input"},
                         {boost::none, "/example-schema:myRpc/output"},
+                        {boost::none, "/example-schema:rpcOneOutput"},
+                        {boost::none, "/example-schema:rpcOneOutput/input"},
+                        {boost::none, "/example-schema:rpcOneOutput/output"},
+                        {boost::none, "/example-schema:rpcOneOutput/output/ahoj"},
+                        {boost::none, "/example-schema:rpcOneInput"},
+                        {boost::none, "/example-schema:rpcOneInput/input"},
+                        {boost::none, "/example-schema:rpcOneInput/input/ahoj"},
+                        {boost::none, "/example-schema:rpcOneInput/output"},
                         {boost::none, "/example-schema:numberOrString"},
                         {boost::none, "/example-schema:obsoleteLeaf"},
                         {boost::none, "/example-schema:obsoleteLeafWithDeprecatedType"},
@@ -1135,6 +1163,30 @@
             REQUIRE(ys.dataPathToSchemaPath("/example-schema:portSettings[port='eth0']") == "/example-schema:portSettings");
             REQUIRE(ys.dataPathToSchemaPath("/example-schema:portSettings[port='eth0']/shutdown") == "/example-schema:portSettings/shutdown");
         }
+
+        SECTION("has input nodes")
+        {
+            bool expected;
+            SECTION("example-schema:myRpc")
+            {
+                path.m_nodes.emplace_back(module_{"example-schema"}, rpcNode_{"myRpc"});
+                expected = false;
+            }
+
+            SECTION("example-schema:rpcOneInput")
+            {
+                path.m_nodes.emplace_back(module_{"example-schema"}, rpcNode_{"rpcOneInput"});
+                expected = true;
+            }
+
+            SECTION("example-schema:rpcOneOutput")
+            {
+                path.m_nodes.emplace_back(module_{"example-schema"}, rpcNode_{"rpcOneOutput"});
+                expected = false;
+            }
+
+            REQUIRE(ys.hasInputNodes(pathToSchemaString(path, Prefixes::WhenNeeded)) == expected);
+        }
     }
 
     SECTION("negative")