Merge changes Ib455f532,I2d252609
* changes:
Add support for executing RPCs
Add ProxyDatastore
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 4fd1b0d..e407ba2 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -133,10 +133,15 @@
)
target_link_libraries(parser schemas utils ast_values)
+add_library(proxydatastore STATIC
+ src/proxy_datastore.cpp
+ )
+target_link_libraries(proxydatastore PUBLIC datastoreaccess yangaccess)
+
# Links libraries, that aren't specific to a datastore type
function(cli_link_required cli_target)
target_include_directories(${cli_target} PRIVATE ${REPLXX_PATH})
- target_link_libraries(${cli_target} yangschema docopt parser ${REPLXX_LIBRARY})
+ target_link_libraries(${cli_target} proxydatastore yangschema docopt parser ${REPLXX_LIBRARY})
add_dependencies(${cli_target} target-NETCONF_CLI_VERSION)
target_include_directories(${cli_target} PRIVATE ${PROJECT_BINARY_DIR})
endfunction()
@@ -277,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()
@@ -302,6 +307,7 @@
target_link_libraries(test_set_value_completion leaf_data_type)
cli_test(list_manipulation)
cli_test(interpreter)
+ target_link_libraries(test_interpreter proxydatastore)
cli_test(path_utils)
target_link_libraries(test_path_utils path)
cli_test(keyvalue_completion)
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 8a3536f..a539b90 100644
--- a/src/cli.cpp
+++ b/src/cli.cpp
@@ -12,8 +12,10 @@
#include <sstream>
#include "NETCONF_CLI_VERSION.h"
#include "interpreter.hpp"
+#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
@@ -72,10 +74,10 @@
return 1;
}
}
- SysrepoAccess datastore(PROGRAM_NAME, datastoreType);
+ auto datastore = std::make_shared<SysrepoAccess>(PROGRAM_NAME, datastoreType);
std::cout << "Connected to sysrepo [datastore: " << (datastoreType == Datastore::Startup ? "startup" : "running") << "]" << std::endl;
#elif defined(YANG_CLI)
- YangAccess datastore;
+ auto datastore = std::make_shared<YangAccess>();
if (args["--configonly"].asBool()) {
writableOps = WritableOps::No;
} else {
@@ -83,13 +85,13 @@
std::cout << "ops is writable" << std::endl;
}
if (const auto& search_dir = args["-s"]) {
- datastore.addSchemaDir(search_dir.asString());
+ datastore->addSchemaDir(search_dir.asString());
}
for (const auto& schemaFile : args["<schema_file_or_module_name>"].asStringList()) {
if (std::filesystem::exists(schemaFile)) {
- datastore.addSchemaFile(schemaFile);
+ datastore->addSchemaFile(schemaFile);
} else if (schemaFile.find('/') == std::string::npos) { // Module names cannot have a slash
- datastore.loadModule(schemaFile);
+ datastore->loadModule(schemaFile);
} else {
std::cerr << "Cannot load YANG module " << schemaFile << "\n";
}
@@ -106,7 +108,7 @@
return 1;
}
try {
- datastore.enableFeature(parsed.first, parsed.second);
+ datastore->enableFeature(parsed.first, parsed.second);
} catch (std::runtime_error& ex) {
std::cerr << ex.what() << "\n";
return 1;
@@ -116,15 +118,26 @@
}
if (const auto& dataFiles = args["-i"]) {
for (const auto& dataFile : dataFiles.asStringList()) {
- datastore.addDataFile(dataFile);
+ datastore->addDataFile(dataFile);
}
}
#else
#error "Unknown CLI backend"
#endif
- auto dataQuery = std::make_shared<DataQuery>(datastore);
- Parser parser(datastore.schema(), writableOps, dataQuery);
+#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);
using replxx::Replxx;
@@ -182,11 +195,13 @@
try {
command_ cmd = parser.parseCommand(line, std::cout);
- boost::apply_visitor(Interpreter(parser, datastore), cmd);
+ boost::apply_visitor(Interpreter(parser, proxyDatastore), cmd);
} catch (InvalidCommandException& ex) {
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 3b52960..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
@@ -293,7 +322,7 @@
}
}
-Interpreter::Interpreter(Parser& parser, DatastoreAccess& datastore)
+Interpreter::Interpreter(Parser& parser, ProxyDatastore& datastore)
: m_parser(parser)
, m_datastore(datastore)
{
diff --git a/src/interpreter.hpp b/src/interpreter.hpp
index 543fb2e..0807c02 100644
--- a/src/interpreter.hpp
+++ b/src/interpreter.hpp
@@ -9,12 +9,12 @@
#pragma once
#include <boost/variant/static_visitor.hpp>
-#include "datastore_access.hpp"
+#include "proxy_datastore.hpp"
#include "parser.hpp"
struct Interpreter : boost::static_visitor<void> {
- Interpreter(Parser& parser, DatastoreAccess& datastore);
+ Interpreter(Parser& parser, ProxyDatastore& datastore);
void operator()(const commit_&) const;
void operator()(const set_&) const;
@@ -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;
@@ -40,5 +43,5 @@
[[nodiscard]] boost::variant<dataPath_, schemaPath_, module_> toCanonicalPath(const PathType& path) const;
Parser& m_parser;
- DatastoreAccess& m_datastore;
+ ProxyDatastore& m_datastore;
};
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
new file mode 100644
index 0000000..e6878cb
--- /dev/null
+++ b/src/proxy_datastore.cpp
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2020 CESNET, https://photonics.cesnet.cz/
+ *
+ * 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, 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 pickDatastore(path)->getItems(path);
+}
+
+void ProxyDatastore::setLeaf(const std::string& path, leaf_data_ value)
+{
+ pickDatastore(path)->setLeaf(path, value);
+}
+
+void ProxyDatastore::createItem(const std::string& path)
+{
+ pickDatastore(path)->createItem(path);
+}
+
+void ProxyDatastore::deleteItem(const std::string& path)
+{
+ pickDatastore(path)->deleteItem(path);
+}
+
+void ProxyDatastore::moveItem(const std::string& source, std::variant<yang::move::Absolute, yang::move::Relative> move)
+{
+ pickDatastore(source)->moveItem(source, move);
+}
+
+void ProxyDatastore::commitChanges()
+{
+ m_datastore->commitChanges();
+}
+
+void ProxyDatastore::discardChanges()
+{
+ m_datastore->discardChanges();
+}
+
+void ProxyDatastore::copyConfig(const Datastore source, const Datastore destination)
+{
+ m_datastore->copyConfig(source, destination);
+}
+
+std::string ProxyDatastore::dump(const DataFormat format) const
+{
+ 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
new file mode 100644
index 0000000..c15a869
--- /dev/null
+++ b/src/proxy_datastore.hpp
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2020 CESNET, https://photonics.cesnet.cz/
+ *
+ * Written by Václav Kubernát <kubernat@cesnet.cz>
+ *
+*/
+
+#pragma once
+#include "datastore_access.hpp"
+#include "yang_access.hpp"
+
+/*! \class ProxyDatastore
+ * \brief DatastoreAccess wrapper that handles RPC input
+ */
+class ProxyDatastore {
+public:
+ /**
+ * 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);
+ void deleteItem(const std::string& path);
+ void moveItem(const std::string& source, std::variant<yang::move::Absolute, yang::move::Relative> move);
+ void commitChanges();
+ void discardChanges();
+ 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 3b9736b..a88aa48 100644
--- a/tests/interpreter.cpp
+++ b/tests/interpreter.cpp
@@ -38,7 +38,12 @@
{
auto schema = std::make_shared<MockSchema>();
Parser parser(schema);
- MockDatastoreAccess datastore;
+ 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);
std::vector<std::unique_ptr<trompeloeil::expectation>> expectations;
std::vector<command_> toInterpret;
@@ -173,7 +178,7 @@
}
ls_ ls;
ls.m_path = lsArg;
- expectations.emplace_back(NAMED_REQUIRE_CALL(datastore, schema()).RETURN(schema));
+ expectations.emplace_back(NAMED_REQUIRE_CALL(*datastore, schema()).RETURN(schema));
expectations.emplace_back(NAMED_REQUIRE_CALL(*schema, availableNodes(expectedPath, Recursion::NonRecursive)).RETURN(std::set<ModuleNodePair>{}));
toInterpret.emplace_back(ls);
}
@@ -323,7 +328,7 @@
get_ getCmd;
getCmd.m_path = inputPath;
- expectations.emplace_back(NAMED_REQUIRE_CALL(datastore, getItems(expectedPathArg)).RETURN(treeReturned));
+ expectations.emplace_back(NAMED_REQUIRE_CALL(*datastore, getItems(expectedPathArg)).RETURN(treeReturned));
toInterpret.emplace_back(getCmd);
}
@@ -335,22 +340,22 @@
SECTION("list instance")
{
inputPath.m_nodes = {dataNode_{{"mod"}, listElement_{"department", {{"name", "engineering"s}}}}};
- expectations.emplace_back(NAMED_REQUIRE_CALL(datastore, createItem("/mod:department[name='engineering']")));
- expectations.emplace_back(NAMED_REQUIRE_CALL(datastore, deleteItem("/mod:department[name='engineering']")));
+ expectations.emplace_back(NAMED_REQUIRE_CALL(*datastore, createItem("/mod:department[name='engineering']")));
+ expectations.emplace_back(NAMED_REQUIRE_CALL(*datastore, deleteItem("/mod:department[name='engineering']")));
}
SECTION("leaflist instance")
{
inputPath.m_nodes = {dataNode_{{"mod"}, leafListElement_{"addresses", "127.0.0.1"s}}};
- expectations.emplace_back(NAMED_REQUIRE_CALL(datastore, createItem("/mod:addresses[.='127.0.0.1']")));
- expectations.emplace_back(NAMED_REQUIRE_CALL(datastore, deleteItem("/mod:addresses[.='127.0.0.1']")));
+ expectations.emplace_back(NAMED_REQUIRE_CALL(*datastore, createItem("/mod:addresses[.='127.0.0.1']")));
+ expectations.emplace_back(NAMED_REQUIRE_CALL(*datastore, deleteItem("/mod:addresses[.='127.0.0.1']")));
}
SECTION("presence container")
{
inputPath.m_nodes = {dataNode_{{"mod"}, container_{"pContainer"}}};
- expectations.emplace_back(NAMED_REQUIRE_CALL(datastore, createItem("/mod:pContainer")));
- expectations.emplace_back(NAMED_REQUIRE_CALL(datastore, deleteItem("/mod:pContainer")));
+ expectations.emplace_back(NAMED_REQUIRE_CALL(*datastore, createItem("/mod:pContainer")));
+ expectations.emplace_back(NAMED_REQUIRE_CALL(*datastore, deleteItem("/mod:pContainer")));
}
create_ createCmd;
@@ -363,7 +368,7 @@
SECTION("delete a leaf")
{
- expectations.emplace_back(NAMED_REQUIRE_CALL(datastore, deleteItem("/mod:someLeaf")));
+ expectations.emplace_back(NAMED_REQUIRE_CALL(*datastore, deleteItem("/mod:someLeaf")));
delete_ deleteCmd;
deleteCmd.m_path = {Scope::Absolute, {dataNode_{{"mod"}, leaf_{"someLeaf"}}, }};
toInterpret.emplace_back(deleteCmd);
@@ -371,13 +376,13 @@
SECTION("commit")
{
- expectations.emplace_back(NAMED_REQUIRE_CALL(datastore, commitChanges()));
+ expectations.emplace_back(NAMED_REQUIRE_CALL(*datastore, commitChanges()));
toInterpret.emplace_back(commit_{});
}
SECTION("discard")
{
- expectations.emplace_back(NAMED_REQUIRE_CALL(datastore, discardChanges()));
+ expectations.emplace_back(NAMED_REQUIRE_CALL(*datastore, discardChanges()));
toInterpret.emplace_back(discard_{});
}
@@ -391,7 +396,7 @@
{
inputPath.m_nodes = {dataNode_{{"mod"}, leaf_{"animal"}}};
inputData = identityRef_{"Doge"};
- expectations.emplace_back(NAMED_REQUIRE_CALL(datastore, setLeaf("/mod:animal", identityRef_{"mod", "Doge"})));
+ expectations.emplace_back(NAMED_REQUIRE_CALL(*datastore, setLeaf("/mod:animal", identityRef_{"mod", "Doge"})));
}
@@ -406,18 +411,59 @@
{
SECTION("running -> startup")
{
- expectations.emplace_back(NAMED_REQUIRE_CALL(datastore, copyConfig(Datastore::Running, Datastore::Startup)));
+ expectations.emplace_back(NAMED_REQUIRE_CALL(*datastore, copyConfig(Datastore::Running, Datastore::Startup)));
toInterpret.emplace_back(copy_{{}, Datastore::Running, Datastore::Startup});
}
SECTION("startup -> running")
{
- expectations.emplace_back(NAMED_REQUIRE_CALL(datastore, copyConfig(Datastore::Startup, Datastore::Running)));
+ expectations.emplace_back(NAMED_REQUIRE_CALL(*datastore, copyConfig(Datastore::Startup, Datastore::Running)));
toInterpret.emplace_back(copy_{{}, Datastore::Startup, Datastore::Running});
}
}
for (const auto& command : toInterpret) {
- boost::apply_visitor(Interpreter(parser, datastore), command);
+ 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}));
}