Add support for executing RPCs

Creating a temporary YangAccess for RPC input means I need to somehow
give the right libyang schemas. For that reason I supply a callable
which is able to fetch the schema and create a YangAccess instance for
ProxyDatastore.

The ProxyDatastore class now has a simple mechanism for deciding whether
to use the normal datastore and the temporary based on a path prefix.

Change-Id: Ib455f53237598bc2620161a44fb89c48ddfeb6e3
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 4081ce1..e407ba2 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -136,7 +136,7 @@
 add_library(proxydatastore STATIC
     src/proxy_datastore.cpp
     )
-target_link_libraries(proxydatastore PUBLIC datastoreaccess)
+target_link_libraries(proxydatastore PUBLIC datastoreaccess yangaccess)
 
 # Links libraries, that aren't specific to a datastore type
 function(cli_link_required cli_target)
@@ -282,7 +282,7 @@
         else()
             message(FATAL_ERROR "Unknown backend ${backend}")
         endif()
-        target_link_libraries(${TESTNAME} yangschema sysreposubscription)
+        target_link_libraries(${TESTNAME} yangschema sysreposubscription proxydatastore)
 
         target_compile_definitions(${TESTNAME} PRIVATE ${backend}_BACKEND)
     endfunction()
diff --git a/src/ast_commands.hpp b/src/ast_commands.hpp
index 3f7eead..a594228 100644
--- a/src/ast_commands.hpp
+++ b/src/ast_commands.hpp
@@ -219,8 +219,49 @@
     DataFormat m_format;
 };
 
+struct rpc_ : x3::position_tagged {
+    static constexpr auto name = "rpc";
+    static constexpr auto shortHelp = "rpc - initiate RPC";
+    static constexpr auto longHelp = R"(
+    rpc <rpc-path>
+
+    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;
+
+    dataPath_ m_path;
+};
+
+struct exec_ : x3::position_tagged {
+    static constexpr auto name = "exec";
+    static constexpr auto shortHelp = "exec - execute RPC";
+    static constexpr auto longHelp = R"(
+    exec
+
+    This command executes the RPC you have previously initiated.
+
+    Usage:
+        /> exec)";
+    bool operator==(const exec_& other) const;
+};
+
+struct cancel_ : x3::position_tagged {
+    static constexpr auto name = "cancel";
+    static constexpr auto shortHelp = "cancel - cancel an ongoing RPC input";
+    static constexpr auto longHelp = R"(
+    cancel
+
+    This command cancels a previously entered RPC context.
+
+    Usage:
+        /> cancel)";
+    bool operator==(const cancel_& other) const;
+};
+
 struct help_;
-using CommandTypes = boost::mpl::vector<cd_, commit_, copy_, create_, delete_, describe_, discard_, dump_, get_, help_, ls_, move_, set_>;
+using CommandTypes = boost::mpl::vector<cancel_, cd_, commit_, copy_, create_, delete_, describe_, discard_, dump_, exec_, get_, help_, ls_, move_, rpc_, set_>;
 struct help_ : x3::position_tagged {
     static constexpr auto name = "help";
     static constexpr auto shortHelp = "help - Print help for commands.";
@@ -268,3 +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)
diff --git a/src/ast_handlers.hpp b/src/ast_handlers.hpp
index 08d2079..87cacd1 100644
--- a/src/ast_handlers.hpp
+++ b/src/ast_handlers.hpp
@@ -230,6 +230,10 @@
     }
 };
 
+struct rpcPath_class;
+
+struct cdPath_class;
+
 struct set_class {
     template <typename Iterator, typename Exception, typename Context>
     x3::error_handler_result on_error(Iterator&, Iterator const&, Exception const& x, Context const& context)
@@ -256,6 +260,12 @@
 
 struct dump_class;
 
+struct rpc_class;
+
+struct exec_class;
+
+struct cancel_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_path.cpp b/src/ast_path.cpp
index 44c98c3..e8bfbcf 100644
--- a/src/ast_path.cpp
+++ b/src/ast_path.cpp
@@ -110,6 +110,11 @@
     return (this->m_name == b.m_name);
 }
 
+bool rpcNode_::operator==(const rpcNode_& 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 4e21167..d02be60 100644
--- a/src/ast_path.hpp
+++ b/src/ast_path.hpp
@@ -81,9 +81,15 @@
     std::string m_name;
 };
 
+struct rpcNode_ {
+    bool operator==(const rpcNode_& other) const;
+
+    std::string m_name;
+};
+
 struct schemaNode_ {
     boost::optional<module_> m_prefix;
-    std::variant<container_, list_, nodeup_, leaf_, leafList_> m_suffix;
+    std::variant<container_, list_, nodeup_, leaf_, leafList_, rpcNode_> m_suffix;
 
     schemaNode_();
     schemaNode_(decltype(m_suffix) node);
@@ -93,7 +99,7 @@
 
 struct dataNode_ {
     boost::optional<module_> m_prefix;
-    std::variant<container_, listElement_, nodeup_, leaf_, leafListElement_, leafList_, list_> m_suffix;
+    std::variant<container_, listElement_, nodeup_, leaf_, leafListElement_, leafList_, list_, rpcNode_> m_suffix;
 
     dataNode_();
     dataNode_(decltype(m_suffix) node);
diff --git a/src/cli.cpp b/src/cli.cpp
index 1f62f78..a539b90 100644
--- a/src/cli.cpp
+++ b/src/cli.cpp
@@ -15,6 +15,7 @@
 #include "proxy_datastore.hpp"
 #if defined(SYSREPO_CLI)
 #include "sysrepo_access.hpp"
+#include "yang_schema.hpp"
 #define PROGRAM_NAME "sysrepo-cli"
 static const auto usage = R"(CLI interface to sysrepo
 
@@ -124,7 +125,17 @@
 #error "Unknown CLI backend"
 #endif
 
-    ProxyDatastore proxyDatastore(datastore);
+#if defined(SYSREPO_CLI)
+    auto createTemporaryDatastore = [](const std::shared_ptr<DatastoreAccess>& datastore) {
+        return std::make_shared<YangAccess>(std::static_pointer_cast<YangSchema>(datastore->schema()));
+    };
+#elif defined(YANG_CLI)
+    auto createTemporaryDatastore = [](const std::shared_ptr<DatastoreAccess>&) {
+        return nullptr;
+    };
+#endif
+
+    ProxyDatastore proxyDatastore(datastore, createTemporaryDatastore);
     auto dataQuery = std::make_shared<DataQuery>(*datastore);
     Parser parser(datastore->schema(), writableOps, dataQuery);
 
@@ -189,6 +200,8 @@
             std::cerr << ex.what() << std::endl;
         } catch (DatastoreException& ex) {
             std::cerr << ex.what() << std::endl;
+        } catch (std::runtime_error& ex) {
+            std::cerr << ex.what() << std::endl;
         }
 
         lineEditor.history_add(line);
diff --git a/src/grammars.hpp b/src/grammars.hpp
index 742115b..ee808d5 100644
--- a/src/grammars.hpp
+++ b/src/grammars.hpp
@@ -29,6 +29,9 @@
 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<exec_class, exec_> const exec = "exec";
+x3::rule<cancel_class, cancel_> const cancel = "cancel";
 x3::rule<command_class, command_> const command = "command";
 
 x3::rule<createCommandSuggestions_class, x3::unused_type> const createCommandSuggestions = "createCommandSuggestions";
@@ -52,7 +55,7 @@
     ls_::name >> *(space_separator >> ls_options) >> -(space_separator >> (anyPath | (module >> "*")));
 
 auto const cd_def =
-    cd_::name >> space_separator > dataPath;
+    cd_::name >> space_separator > cdPath;
 
 auto const create_def =
     create_::name >> space_separator > (presenceContainerPath | listInstancePath | leafListElementPath);
@@ -255,6 +258,15 @@
     }
 } const dump_args;
 
+auto const rpc_def =
+    rpc_::name > space_separator > rpcPath;
+
+auto const exec_def =
+    exec_::name >> x3::attr(exec_{});
+
+auto const cancel_def =
+    cancel_::name >> x3::attr(cancel_{});
+
 auto const dump_def =
     dump_::name > space_separator >> dump_args;
 
@@ -262,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];
+    createCommandSuggestions >> x3::expect[cd | copy | create | delete_rule | set | commit | get | ls | discard | describe | help | move | dump | rpc | exec | cancel];
 
 #if __clang__
 #pragma GCC diagnostic pop
@@ -281,5 +293,8 @@
 BOOST_SPIRIT_DEFINE(copy)
 BOOST_SPIRIT_DEFINE(move)
 BOOST_SPIRIT_DEFINE(dump)
+BOOST_SPIRIT_DEFINE(rpc)
+BOOST_SPIRIT_DEFINE(exec)
+BOOST_SPIRIT_DEFINE(cancel)
 BOOST_SPIRIT_DEFINE(command)
 BOOST_SPIRIT_DEFINE(createCommandSuggestions)
diff --git a/src/interpreter.cpp b/src/interpreter.cpp
index 5ff73e7..26e2be4 100644
--- a/src/interpreter.cpp
+++ b/src/interpreter.cpp
@@ -30,6 +30,26 @@
     }
 };
 
+namespace {
+void printTree(const DatastoreAccess::Tree tree)
+{
+    for (auto it = tree.begin(); it != tree.end(); it++) {
+        auto [path, value] = *it;
+        if (value.type() == typeid(special_) && boost::get<special_>(value).m_value == SpecialValue::LeafList) {
+            auto leafListPrefix = path;
+            std::cout << path << " = " << leafDataToString(value) << std::endl;
+
+            while (it + 1 != tree.end() && boost::starts_with((it + 1)->first, leafListPrefix)) {
+                ++it;
+                std::cout << stripLeafListValueFromPath(it->first) << " = " << leafDataToString(it->second) << std::endl;
+            }
+        } else {
+            std::cout << path << " = " << leafDataToString(value) << std::endl;
+        }
+    }
+}
+}
+
 template <typename PathType>
 std::string pathToString(const PathType& path)
 {
@@ -64,20 +84,7 @@
 void Interpreter::operator()(const get_& get) const
 {
     auto items = m_datastore.getItems(pathToString(toCanonicalPath(get.m_path)));
-    for (auto it = items.begin(); it != items.end(); it++) {
-        auto [path, value] = *it;
-        if (value.type() == typeid(special_) && boost::get<special_>(value).m_value == SpecialValue::LeafList) {
-            auto leafListPrefix = path;
-            std::cout << path << " = " << leafDataToString(value) << std::endl;
-
-            while (it + 1 != items.end() && boost::starts_with((it + 1) ->first, leafListPrefix)) {
-                ++it;
-                std::cout << stripLeafListValueFromPath(it->first) << " = " << leafDataToString(it->second) << std::endl;
-            }
-        } else {
-            std::cout << path << " = " << leafDataToString(value) << std::endl;
-        }
-    }
+    printTree(items);
 }
 
 void Interpreter::operator()(const cd_& cd) const
@@ -162,12 +169,14 @@
     case yang::NodeTypes::List:
         ss << "list";
         break;
+    case yang::NodeTypes::Rpc:
+        ss << "RPC";
+        break;
     case yang::NodeTypes::Action:
     case yang::NodeTypes::AnyXml:
     case yang::NodeTypes::LeafList:
     case yang::NodeTypes::Notification:
-    case yang::NodeTypes::Rpc:
-        throw std::logic_error("describe got an rpc or an action: this should never happen, because their paths cannot be parsed");
+        throw std::logic_error("describe can't handle the type of " + path);
     }
 
     if (!m_datastore.schema()->isConfig(path)) {
@@ -201,6 +210,26 @@
     std::cout << m_datastore.dump(dump.m_format) << "\n";
 }
 
+void Interpreter::operator()(const rpc_& rpc) const
+{
+    m_datastore.initiateRpc(pathToString(toCanonicalPath(rpc.m_path)));
+    m_parser.changeNode(rpc.m_path);
+}
+
+void Interpreter::operator()(const exec_&) const
+{
+    m_parser.changeNode({Scope::Absolute, {}});
+    auto output = m_datastore.executeRpc();
+    std::cout << "RPC output:\n";
+    printTree(output);
+}
+
+void Interpreter::operator()(const cancel_&) const
+{
+    m_parser.changeNode({Scope::Absolute, {}});
+    m_datastore.cancelRpc();
+}
+
 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 000cc21..0807c02 100644
--- a/src/interpreter.hpp
+++ b/src/interpreter.hpp
@@ -29,6 +29,9 @@
     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 exec_& exec) const;
+    void operator()(const cancel_& cancel) const;
 
 private:
     [[nodiscard]] std::string buildTypeInfo(const std::string& path) const;
diff --git a/src/path_parser.hpp b/src/path_parser.hpp
index 9fa3db1..00629bb 100644
--- a/src/path_parser.hpp
+++ b/src/path_parser.hpp
@@ -14,6 +14,8 @@
 
 namespace x3 = boost::spirit::x3;
 
+x3::rule<cdPath_class, dataPath_> const cdPath = "cdPath";
+x3::rule<rpcPath_class, dataPath_> const rpcPath = "rpcPath";
 x3::rule<presenceContainerPath_class, dataPath_> const presenceContainerPath = "presenceContainerPath";
 x3::rule<listInstancePath_class, dataPath_> const listInstancePath = "listInstancePath";
 x3::rule<leafListElementPath_class, dataPath_> const leafListElementPath = "leafListElementPath";
@@ -137,10 +139,13 @@
                         parserContext.m_suggestions.emplace(Completion{parseString, "[", Completion::WhenToAdd::IfFullMatch});
                     }
                     break;
+                case yang::NodeTypes::Rpc:
+                    out.m_suffix = rpcNode_{child.second};
+                    parserContext.m_suggestions.emplace(Completion{parseString + "/"});
+                    break;
                 case yang::NodeTypes::Action:
                 case yang::NodeTypes::AnyXml:
                 case yang::NodeTypes::Notification:
-                case yang::NodeTypes::Rpc:
                     continue;
             }
             table.add(parseString, out);
@@ -421,6 +426,20 @@
 auto const writableLeafPath_def =
     PathParser<PathParserMode::DataPath, CompletionMode::Data>{filterConfigFalse};
 
+auto const onlyRpc = [] (const Schema& schema, const std::string& path) {
+    return schema.nodeType(path) == yang::NodeTypes::Rpc;
+};
+
+auto const rpcPath_def =
+    PathParser<PathParserMode::DataPath, CompletionMode::Data>{onlyRpc};
+
+auto const noRpc = [] (const Schema& schema, const std::string& path) {
+    return schema.nodeType(path) != yang::NodeTypes::Rpc;
+};
+
+auto const cdPath_def =
+    PathParser<PathParserMode::DataPath, CompletionMode::Data>{noRpc};
+
 auto const presenceContainerPath_def =
     dataPath;
 
@@ -443,6 +462,8 @@
 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)
 BOOST_SPIRIT_DEFINE(leafListElementPath)
diff --git a/src/proxy_datastore.cpp b/src/proxy_datastore.cpp
index ab9193c..e6878cb 100644
--- a/src/proxy_datastore.cpp
+++ b/src/proxy_datastore.cpp
@@ -4,36 +4,39 @@
  * Written by Václav Kubernát <kubernat@cesnet.cz>
  *
 */
+#include <boost/algorithm/string/predicate.hpp>
 #include "proxy_datastore.hpp"
+#include "yang_schema.hpp"
 
-ProxyDatastore::ProxyDatastore(const std::shared_ptr<DatastoreAccess>& datastore)
+ProxyDatastore::ProxyDatastore(const std::shared_ptr<DatastoreAccess>& datastore, std::function<std::shared_ptr<DatastoreAccess>(const std::shared_ptr<DatastoreAccess>&)> createTemporaryDatastore)
     : m_datastore(datastore)
+    , m_createTemporaryDatastore(createTemporaryDatastore)
 {
 }
 
 DatastoreAccess::Tree ProxyDatastore::getItems(const std::string& path) const
 {
-    return m_datastore->getItems(path);
+    return pickDatastore(path)->getItems(path);
 }
 
 void ProxyDatastore::setLeaf(const std::string& path, leaf_data_ value)
 {
-    m_datastore->setLeaf(path, value);
+    pickDatastore(path)->setLeaf(path, value);
 }
 
 void ProxyDatastore::createItem(const std::string& path)
 {
-    m_datastore->createItem(path);
+    pickDatastore(path)->createItem(path);
 }
 
 void ProxyDatastore::deleteItem(const std::string& path)
 {
-    m_datastore->deleteItem(path);
+    pickDatastore(path)->deleteItem(path);
 }
 
 void ProxyDatastore::moveItem(const std::string& source, std::variant<yang::move::Absolute, yang::move::Relative> move)
 {
-    m_datastore->moveItem(source, move);
+    pickDatastore(source)->moveItem(source, move);
 }
 
 void ProxyDatastore::commitChanges()
@@ -56,7 +59,41 @@
     return m_datastore->dump(format);
 }
 
+void ProxyDatastore::initiateRpc(const std::string& rpcPath)
+{
+    if (m_inputDatastore) {
+        throw std::runtime_error("RPC input already in progress (" + m_rpcPath + ")");
+    }
+    m_inputDatastore = m_createTemporaryDatastore(m_datastore);
+    m_rpcPath = rpcPath;
+    m_inputDatastore->createItem(rpcPath);
+}
+
+DatastoreAccess::Tree ProxyDatastore::executeRpc()
+{
+    if (!m_inputDatastore) {
+        throw std::runtime_error("No RPC input in progress");
+    }
+    auto inputData = m_inputDatastore->getItems("/");
+    m_inputDatastore = nullptr;
+    return m_datastore->executeRpc(m_rpcPath, inputData);
+}
+
+void ProxyDatastore::cancelRpc()
+{
+    m_inputDatastore = nullptr;
+}
+
 std::shared_ptr<Schema> ProxyDatastore::schema() const
 {
     return m_datastore->schema();
 }
+
+std::shared_ptr<DatastoreAccess> ProxyDatastore::pickDatastore(const std::string& path) const
+{
+    if (!m_inputDatastore || !boost::starts_with(path, m_rpcPath)) {
+        return m_datastore;
+    } else {
+        return m_inputDatastore;
+    }
+}
diff --git a/src/proxy_datastore.hpp b/src/proxy_datastore.hpp
index 0ed5e2d..c15a869 100644
--- a/src/proxy_datastore.hpp
+++ b/src/proxy_datastore.hpp
@@ -7,13 +7,18 @@
 
 #pragma once
 #include "datastore_access.hpp"
+#include "yang_access.hpp"
 
 /*! \class ProxyDatastore
  *     \brief DatastoreAccess wrapper that handles RPC input
  */
 class ProxyDatastore {
 public:
-    ProxyDatastore(const std::shared_ptr<DatastoreAccess>& datastore);
+    /**
+     * The createTemporaryDatastore function should create a temporary datastore that's going to be used for RPC input.
+     * This temporary datastore and the main datastore are supposed share the same schemas.
+     * */
+    ProxyDatastore(const std::shared_ptr<DatastoreAccess>& datastore, std::function<std::shared_ptr<DatastoreAccess>(const std::shared_ptr<DatastoreAccess>&)> createTemporaryDatastore);
     [[nodiscard]] DatastoreAccess::Tree getItems(const std::string& path) const;
     void setLeaf(const std::string& path, leaf_data_ value);
     void createItem(const std::string& path);
@@ -24,7 +29,21 @@
     void copyConfig(const Datastore source, const Datastore destination);
     [[nodiscard]] std::string dump(const DataFormat format) const;
 
+    void initiateRpc(const std::string& rpcPath);
+    [[nodiscard]] DatastoreAccess::Tree executeRpc();
+    void cancelRpc();
+
     [[nodiscard]] std::shared_ptr<Schema> schema() const;
 private:
+    /** @brief Picks a datastore based on the requested path.
+     *
+     * If the path starts with a currently processed RPC, m_inputDatastore is picked.
+     * Otherwise m_datastore is picked.
+     */
+    [[nodiscard]] std::shared_ptr<DatastoreAccess> pickDatastore(const std::string& path) const;
+
     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;
 };
diff --git a/src/static_schema.cpp b/src/static_schema.cpp
index 75ccb4f..e54ca68 100644
--- a/src/static_schema.cpp
+++ b/src/static_schema.cpp
@@ -34,6 +34,15 @@
     m_nodes.emplace(key, std::unordered_map<std::string, NodeInfo>());
 }
 
+void StaticSchema::addRpc(const std::string& location, const std::string& name)
+{
+    m_nodes.at(location).emplace(name, NodeInfo{yang::rpc{}, yang::AccessType::Writable});
+
+    //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>());
+}
+
 bool StaticSchema::listHasKey(const schemaPath_& listPath, const std::string& key) const
 {
     return listKeys(listPath).count(key);
@@ -201,6 +210,11 @@
     {
         return yang::NodeTypes::LeafList;
     }
+
+    yang::NodeTypes operator()(const yang::rpc)
+    {
+        return yang::NodeTypes::Rpc;
+    }
 };
 
 yang::NodeTypes StaticSchema::nodeType(const schemaPath_& location, const ModuleNodePair& node) const
diff --git a/src/static_schema.hpp b/src/static_schema.hpp
index 9d897b1..e343087 100644
--- a/src/static_schema.hpp
+++ b/src/static_schema.hpp
@@ -34,6 +34,9 @@
     yang::TypeInfo m_type;
 };
 
+struct rpc {
+};
+
 struct module {
 };
 
@@ -43,7 +46,7 @@
 };
 }
 
-using NodeType = std::variant<yang::container, yang::list, yang::leaf, yang::leaflist>;
+using NodeType = std::variant<yang::container, yang::list, yang::leaf, yang::leaflist, yang::rpc>;
 
 struct NodeInfo {
     NodeType m_nodeType;
@@ -83,6 +86,7 @@
     void addLeaf(const std::string& location, const std::string& name, const yang::LeafDataType& type, const yang::AccessType accessType = yang::AccessType::Writable);
     void addLeafList(const std::string& location, const std::string& name, const yang::LeafDataType& type);
     void addList(const std::string& location, const std::string& name, const std::set<std::string>& keys);
+    void addRpc(const std::string& location, const std::string& name);
     void addModule(const std::string& name);
     void addIdentity(const std::optional<identityRef_>& base, const identityRef_& name);
 
diff --git a/src/sysrepo_access.cpp b/src/sysrepo_access.cpp
index 0fac1d8..510c530 100644
--- a/src/sysrepo_access.cpp
+++ b/src/sysrepo_access.cpp
@@ -132,7 +132,16 @@
 
     void operator()(const special_& value) const
     {
-        throw std::runtime_error("Tried constructing S_Val from a " + specialValueToString(value));
+        switch (value.m_value) {
+        case SpecialValue::PresenceContainer:
+            v->set(xpath.c_str(), nullptr, SR_CONTAINER_PRESENCE_T);
+            break;
+        case SpecialValue::List:
+            v->set(xpath.c_str(), nullptr, SR_LIST_T);
+            break;
+        default:
+            throw std::runtime_error("Tried constructing S_Val from a " + specialValueToString(value));
+        }
     }
 
     void operator()(const std::string& value) const
diff --git a/src/yang_access.cpp b/src/yang_access.cpp
index 8277ef1..ebe39f1 100644
--- a/src/yang_access.cpp
+++ b/src/yang_access.cpp
@@ -31,6 +31,15 @@
     : m_ctx(lyWrap(ly_ctx_new(nullptr, LY_CTX_DISABLE_SEARCHDIR_CWD)))
     , m_datastore(lyWrap<lyd_node>(nullptr))
     , m_schema(std::make_shared<YangSchema>(libyang::create_new_Context(m_ctx.get())))
+    , m_validation_mode(LYD_OPT_DATA)
+{
+}
+
+YangAccess::YangAccess(std::shared_ptr<YangSchema> schema)
+    : m_ctx(schema->m_context->swig_ctx(), [](auto) {})
+    , m_datastore(lyWrap<lyd_node>(nullptr))
+    , m_schema(schema)
+    , m_validation_mode(LYD_OPT_RPC)
 {
 }
 
@@ -103,7 +112,12 @@
 void YangAccess::validate()
 {
     auto datastore = m_datastore.release();
-    lyd_validate(&datastore, LYD_OPT_DATA | LYD_OPT_DATA_NO_YANGLIB, m_ctx.get());
+
+    if (m_validation_mode == LYD_OPT_RPC) {
+        lyd_validate(&datastore, m_validation_mode, nullptr);
+    } else {
+        lyd_validate(&datastore, m_validation_mode | LYD_OPT_DATA_NO_YANGLIB, m_ctx.get());
+    }
     m_datastore = lyWrap(datastore);
 }
 
@@ -116,8 +130,12 @@
 
     auto set = lyWrap(lyd_find_path(m_datastore.get(), path == "/" ? "/*" : path.c_str()));
     auto setWrapper = libyang::Set(set.get(), nullptr);
-
-    lyNodesToTree(res, setWrapper.data());
+    std::optional<std::string> ignoredXPathPrefix;
+    if (m_datastore->schema->nodetype == LYS_RPC) {
+        auto path = std::unique_ptr<char, decltype(&free)>(lys_path(m_datastore->schema, 0), &free);
+        ignoredXPathPrefix = joinPaths(path.get(), "/");
+    }
+    lyNodesToTree(res, setWrapper.data(), ignoredXPathPrefix);
     return res;
 }
 
@@ -218,6 +236,9 @@
         getErrorsAndThrow();
     }
     for (const auto& [k, v] : input) {
+        if (v.type() == typeid(special_) && boost::get<special_>(v).m_value != SpecialValue::PresenceContainer) {
+            continue;
+        }
         auto node = lyd_new_path(root.get(), m_ctx.get(), joinPaths(path, k).c_str(), (void*)leafDataToString(v).c_str(), LYD_ANYDATA_CONSTSTRING, 0);
         if (!node) {
             getErrorsAndThrow();
diff --git a/src/yang_access.hpp b/src/yang_access.hpp
index 075ba26..503a077 100644
--- a/src/yang_access.hpp
+++ b/src/yang_access.hpp
@@ -21,6 +21,7 @@
 class YangAccess : public DatastoreAccess {
 public:
     YangAccess();
+    YangAccess(std::shared_ptr<YangSchema> schema);
     ~YangAccess() override;
     [[nodiscard]] Tree getItems(const std::string& path) const override;
     void setLeaf(const std::string& path, leaf_data_ value) override;
@@ -53,4 +54,5 @@
     std::unique_ptr<ly_ctx, void(*)(ly_ctx*)> m_ctx;
     std::unique_ptr<lyd_node, void(*)(lyd_node*)> m_datastore;
     std::shared_ptr<YangSchema> m_schema;
+    const int m_validation_mode;
 };
diff --git a/src/yang_schema.cpp b/src/yang_schema.cpp
index 711cdbd..05195b9 100644
--- a/src/yang_schema.cpp
+++ b/src/yang_schema.cpp
@@ -101,7 +101,11 @@
             [&oldOptions]() {
                 libyang::set_log_options(oldOptions);
             });
-        return m_context->get_node(nullptr, node.c_str());
+        auto res = m_context->get_node(nullptr, node.c_str());
+        if (!res) { // If no node is found, try output rpc nodes too.
+            res = m_context->get_node(nullptr, node.c_str(), 1);
+        }
+        return res;
     }
 }
 
@@ -476,7 +480,20 @@
 
 bool YangSchema::isConfig(const std::string& path) const
 {
-    return getSchemaNode(path.c_str())->flags() & LYS_CONFIG_W;
+    auto node = getSchemaNode(path.c_str());
+    if (node->flags() & LYS_CONFIG_W) {
+        return true;
+    }
+
+    // Node can still be an input node.
+    while (node->parent()) {
+        node = node->parent();
+        if (node->nodetype() == LYS_INPUT) {
+            return true;
+        }
+    }
+
+    return false;
 }
 
 std::optional<std::string> YangSchema::defaultValue(const std::string& leafPath) const
diff --git a/src/yang_schema.hpp b/src/yang_schema.hpp
index a527ef3..5f3aa20 100644
--- a/src/yang_schema.hpp
+++ b/src/yang_schema.hpp
@@ -70,6 +70,7 @@
     std::shared_ptr<libyang::Module> getYangModule(const std::string& name);
 
 private:
+    friend class YangAccess;
     template <typename NodeType>
     [[nodiscard]] yang::TypeInfo impl_leafType(const std::shared_ptr<libyang::Schema_Node>& node) const;
     [[nodiscard]] std::set<std::string> modules() const;
diff --git a/tests/cd.cpp b/tests/cd.cpp
index 0c5411b..307f287 100644
--- a/tests/cd.cpp
+++ b/tests/cd.cpp
@@ -29,6 +29,7 @@
     schema->addList("/", "example:twoKeyList", {"number", "name"});
     schema->addLeaf("/example:twoKeyList", "example:number", yang::Int32{});
     schema->addLeaf("/example:twoKeyList", "example:name", yang::String{});
+    schema->addRpc("/", "example:launch-nukes");
     Parser parser(schema);
     std::string input;
     std::ostringstream errorStream;
@@ -313,6 +314,11 @@
             input = "cd example:list  [number=10]";
         }
 
+        SECTION("cd into rpc")
+        {
+            input = "cd example:launch-nukes";
+        }
+
         REQUIRE_THROWS_AS(parser.parseCommand(input, errorStream), InvalidCommandException);
     }
 }
diff --git a/tests/command_completion.cpp b/tests/command_completion.cpp
index cf6b188..49017b0 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"};
+        expectedCompletions = {"cd", "copy", "create", "delete", "set", "commit", "get", "ls", "discard", "help", "describe", "move", "dump", "rpc", "exec", "cancel"};
         expectedContextLength = 0;
         SECTION("no space") {
             input = "";
@@ -34,7 +34,7 @@
     SECTION("c")
     {
         input = "c";
-        expectedCompletions = {"cd", "commit", "copy", "create"};
+        expectedCompletions = {"cd", "commit", "copy", "create", "cancel"};
         expectedContextLength = 1;
     }
 
diff --git a/tests/datastore_access.cpp b/tests/datastore_access.cpp
index b45f027..fb44018 100644
--- a/tests/datastore_access.cpp
+++ b/tests/datastore_access.cpp
@@ -8,6 +8,8 @@
 
 #include "trompeloeil_doctest.hpp"
 #include <sysrepo-cpp/Session.hpp>
+#include "yang_schema.hpp"
+#include "proxy_datastore.hpp"
 
 #ifdef sysrepo_BACKEND
 #include "sysrepo_access.hpp"
@@ -831,9 +833,11 @@
     int rpc(const char *xpath, const ::sysrepo::S_Vals input, ::sysrepo::S_Vals_Holder output, void *) override
     {
         const auto nukes = "/example-schema:launch-nukes"s;
-        if (xpath == "/example-schema:noop"s) {
+        if (xpath == "/example-schema:noop"s || xpath == "/example-schema:fire"s) {
             return SR_ERR_OK;
-        } else if (xpath == nukes) {
+        }
+
+        if (xpath == nukes) {
             uint64_t kilotons = 0;
             bool hasCities = false;
             for (size_t i = 0; i < input->val_cnt(); ++i) {
@@ -879,21 +883,32 @@
     auto srSubscription = std::make_shared<sysrepo::Subscribe>(srSession);
     auto cb = std::make_shared<RpcCb>();
     sysrepo::Logs{}.set_stderr(SR_LL_INF);
+    auto doNothingCb = std::make_shared<sysrepo::Callback>();
+    srSubscription->module_change_subscribe("example-schema", doNothingCb, nullptr, SR_SUBSCR_CTX_REUSE);
+    // careful here, sysrepo insists on module_change CBs being registered before RPC CBs, otherwise there's a memleak
     srSubscription->rpc_subscribe("/example-schema:noop", cb, nullptr, SR_SUBSCR_CTX_REUSE);
     srSubscription->rpc_subscribe("/example-schema:launch-nukes", cb, nullptr, SR_SUBSCR_CTX_REUSE);
+    srSubscription->rpc_subscribe("/example-schema:fire", cb, nullptr, SR_SUBSCR_CTX_REUSE);
 
 #ifdef sysrepo_BACKEND
-    SysrepoAccess datastore("netconf-cli-test", Datastore::Running);
+    auto datastore = std::make_shared<SysrepoAccess>("netconf-cli-test", Datastore::Running);
 #elif defined(netconf_BACKEND)
-    NetconfAccess datastore(NETOPEER_SOCKET_PATH);
+    auto datastore = std::make_shared<NetconfAccess>(NETOPEER_SOCKET_PATH);
 #elif defined(yang_BACKEND)
-    YangAccess datastore;
-    datastore.addSchemaDir(schemaDir);
-    datastore.addSchemaFile(exampleSchemaFile);
+    auto datastore = std::make_shared<YangAccess>();
+    datastore->addSchemaDir(schemaDir);
+    datastore->addSchemaFile(exampleSchemaFile);
 #else
 #error "Unknown backend"
 #endif
 
+    auto createTemporaryDatastore = [](const std::shared_ptr<DatastoreAccess>& datastore) {
+        return std::make_shared<YangAccess>(std::static_pointer_cast<YangSchema>(datastore->schema()));
+    };
+
+    ProxyDatastore proxyDatastore(datastore, createTemporaryDatastore);
+
+    // ProxyDatastore cannot easily read DatastoreAccess::Tree, so we need to set the input via create/setLeaf/etc.
     SECTION("valid")
     {
         std::string rpc;
@@ -901,6 +916,7 @@
 
         SECTION("noop") {
             rpc = "/example-schema:noop";
+            proxyDatastore.initiateRpc(rpc);
         }
 
         SECTION("small nuke") {
@@ -909,6 +925,8 @@
                 {"description", "dummy"s},
                 {"payload/kilotons", uint64_t{333'666}},
             };
+            proxyDatastore.initiateRpc(rpc);
+            proxyDatastore.setLeaf("/example-schema:launch-nukes/example-schema:payload/example-schema:kilotons", uint64_t{333'666});
             // no data are returned
         }
 
@@ -918,6 +936,9 @@
                 {"description", "dummy"s},
                 {"payload/kilotons", uint64_t{4}},
             };
+            proxyDatastore.initiateRpc(rpc);
+            proxyDatastore.setLeaf("/example-schema:launch-nukes/example-schema:payload/example-schema:kilotons", uint64_t{4});
+
             output = {
                 {"blast-radius", uint32_t{33'666}},
                 {"actual-yield", uint64_t{5}},
@@ -930,6 +951,9 @@
                 {"payload/kilotons", uint64_t{6}},
                 {"cities/targets[city='Prague']/city", "Prague"s},
             };
+            proxyDatastore.initiateRpc(rpc);
+            proxyDatastore.setLeaf("/example-schema:launch-nukes/example-schema:payload/example-schema:kilotons", uint64_t{6});
+            proxyDatastore.createItem("/example-schema:launch-nukes/example-schema:cities/example-schema:targets[city='Prague']");
             output = {
                 {"blast-radius", uint32_t{33'666}},
                 {"actual-yield", uint64_t{7}},
@@ -941,12 +965,25 @@
             };
         }
 
-        catching<OnRPC>([&] {REQUIRE(datastore.executeRpc(rpc, input) == output);});
+        SECTION("with leafref") {
+            datastore->createItem("/example-schema:person[name='Colton']");
+            datastore->commitChanges();
+
+            rpc = "/example-schema:fire";
+            input = {
+                {"whom", "Colton"s}
+            };
+            proxyDatastore.initiateRpc(rpc);
+            proxyDatastore.setLeaf("/example-schema:fire/example-schema:whom", "Colton"s);
+        }
+
+        catching<OnRPC>([&] {REQUIRE(datastore->executeRpc(rpc, input) == output);});
+        catching<OnRPC>([&] {REQUIRE(proxyDatastore.executeRpc() == output);});
     }
 
     SECTION("non-existing RPC")
     {
-        catching<OnInvalidRpcPath>([&] {datastore.executeRpc("/example-schema:non-existing", DatastoreAccess::Tree{});});
+        catching<OnInvalidRpcPath>([&] {datastore->executeRpc("/example-schema:non-existing", DatastoreAccess::Tree{});});
     }
 
     waitForCompletionAndBitMore(seq1);
diff --git a/tests/example-schema.yang b/tests/example-schema.yang
index 792ac42..04ae22d 100644
--- a/tests/example-schema.yang
+++ b/tests/example-schema.yang
@@ -111,6 +111,16 @@
         }
     }
 
+    rpc fire {
+        input {
+            leaf whom {
+                type leafref {
+                    path '/aha:person/name';
+                }
+            }
+        }
+    }
+
     rpc launch-nukes {
         input {
             container payload {
diff --git a/tests/interpreter.cpp b/tests/interpreter.cpp
index 89fbd29..a88aa48 100644
--- a/tests/interpreter.cpp
+++ b/tests/interpreter.cpp
@@ -39,7 +39,11 @@
     auto schema = std::make_shared<MockSchema>();
     Parser parser(schema);
     auto datastore = std::make_shared<MockDatastoreAccess>();
-    ProxyDatastore proxyDatastore(datastore);
+    auto input_datastore = std::make_shared<MockDatastoreAccess>();
+    auto createTemporaryDatastore = [input_datastore]([[maybe_unused]] const std::shared_ptr<DatastoreAccess>& datastore) {
+        return input_datastore;
+    };
+    ProxyDatastore proxyDatastore(datastore, createTemporaryDatastore);
     std::vector<std::unique_ptr<trompeloeil::expectation>> expectations;
 
     std::vector<command_> toInterpret;
@@ -422,3 +426,44 @@
         boost::apply_visitor(Interpreter(parser, proxyDatastore), command);
     }
 }
+
+TEST_CASE("rpc")
+{
+    auto schema = std::make_shared<MockSchema>();
+    Parser parser(schema);
+    auto datastore = std::make_shared<MockDatastoreAccess>();
+    auto input_datastore = std::make_shared<MockDatastoreAccess>();
+    auto createTemporaryDatastore = [input_datastore]([[maybe_unused]] const std::shared_ptr<DatastoreAccess>& datastore) {
+        return input_datastore;
+    };
+    ProxyDatastore proxyDatastore(datastore, createTemporaryDatastore);
+
+    SECTION("entering/leaving rpc context")
+    {
+        dataPath_ rpcPath;
+        rpcPath.pushFragment({{"example"}, rpcNode_{"launch-nukes"}});
+        rpc_ rpcCmd;
+        rpcCmd.m_path = rpcPath;
+
+        {
+            REQUIRE_CALL(*input_datastore, createItem("/example:launch-nukes"));
+            boost::apply_visitor(Interpreter(parser, proxyDatastore), command_{rpcCmd});
+        }
+
+        REQUIRE(parser.currentPath() == rpcPath);
+
+        SECTION("exec")
+        {
+            REQUIRE_CALL(*input_datastore, getItems("/")).RETURN(DatastoreAccess::Tree{});
+            REQUIRE_CALL(*datastore, executeRpc("/example:launch-nukes", DatastoreAccess::Tree{})).RETURN(DatastoreAccess::Tree{});
+            boost::apply_visitor(Interpreter(parser, proxyDatastore), command_{exec_{}});
+        }
+
+        SECTION("cancel")
+        {
+            boost::apply_visitor(Interpreter(parser, proxyDatastore), command_{cancel_{}});
+        }
+
+        REQUIRE(parser.currentPath() == dataPath_{});
+    }
+}
diff --git a/tests/path_completion.cpp b/tests/path_completion.cpp
index 249dc8c..1c0c8f1 100644
--- a/tests/path_completion.cpp
+++ b/tests/path_completion.cpp
@@ -39,6 +39,8 @@
     schema->addLeaf("/", "example:leafInt", yang::Int32{});
     schema->addLeaf("/", "example:readonly", yang::Int32{}, yang::AccessType::ReadOnly);
     schema->addLeafList("/", "example:addresses", yang::String{});
+    schema->addRpc("/", "second:fire");
+    schema->addLeaf("/second:fire", "second:whom", yang::String{});
     auto mockDatastore = std::make_shared<MockDatastoreAccess>();
 
     // The parser will use DataQuery for key value completion, but I'm not testing that here, so I don't return anything.
@@ -68,7 +70,7 @@
         SECTION("ls ")
         {
             input = "ls ";
-            expectedCompletions = {"example:addresses/", "example:ano/", "example:anoda/", "example:bota/", "example:leafInt ", "example:list/", "example:ovoce/", "example:readonly ", "example:ovocezelenina/", "example:twoKeyList/", "second:amelie/"};
+            expectedCompletions = {"example:addresses/", "example:ano/", "example:anoda/", "example:bota/", "example:leafInt ", "example:list/", "example:ovoce/", "example:readonly ", "example:ovocezelenina/", "example:twoKeyList/", "second:amelie/", "second:fire/"};
             expectedContextLength = 0;
         }
 
@@ -103,7 +105,7 @@
         SECTION("ls /")
         {
             input = "ls /";
-            expectedCompletions = {"example:addresses/", "example:ano/", "example:anoda/", "example:bota/", "example:leafInt ", "example:list/", "example:ovoce/", "example:readonly ", "example:ovocezelenina/", "example:twoKeyList/", "second:amelie/"};
+            expectedCompletions = {"example:addresses/", "example:ano/", "example:anoda/", "example:bota/", "example:leafInt ", "example:list/", "example:ovoce/", "example:readonly ", "example:ovocezelenina/", "example:twoKeyList/", "second:amelie/", "second:fire/"};
             expectedContextLength = 0;
         }
 
@@ -131,7 +133,7 @@
         SECTION("ls /s")
         {
             input = "ls /s";
-            expectedCompletions = {"second:amelie/"};
+            expectedCompletions = {"second:amelie/", "second:fire/"};
             expectedContextLength = 1;
         }
 
@@ -334,5 +336,28 @@
         expectedContextLength = 0;
     }
 
+    SECTION("rpc input nodes NOT completed for rpc command")
+    {
+        input = "rpc example:fire/";
+        expectedCompletions = {};
+        expectedContextLength = 13;
+    }
+
+    SECTION("rpc input nodes completed for set command")
+    {
+        parser.changeNode({{}, {{module_{"second"}, rpcNode_{"fire"}}}});
+        input = "set ";
+        expectedCompletions = {"whom "};
+        expectedContextLength = 0;
+    }
+
+    SECTION("completion for other stuff while inside an rpc")
+    {
+        parser.changeNode({{}, {{module_{"second"}, rpcNode_{"fire"}}}});
+        input = "set ../";
+        expectedCompletions = {"example:addresses", "example:ano/", "example:anoda/", "example:bota/", "example:leafInt ", "example:list", "example:ovoce", "example:ovocezelenina", "example:twoKeyList", "second:amelie/", "second:fire/"};
+        expectedContextLength = 0;
+    }
+
     REQUIRE(parser.completeCommand(input, errorStream) == (Completions{expectedCompletions, expectedContextLength}));
 }