Integrate DataQuery
Change-Id: I439374afe485baf08a4d5a1a02fd70d908bec9e1
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 513c039..34c6fb2 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -237,7 +237,7 @@
tests/${name}.cpp
)
endif()
- target_link_libraries(test_${name} DoctestIntegration parser)
+ target_link_libraries(test_${name} DoctestIntegration parser datastoreaccess)
if(NOT CMAKE_CROSSCOMPILING)
add_test(test_${name} test_${name})
endif()
@@ -281,6 +281,7 @@
cli_test(parser_methods)
cli_test(path_utils)
target_link_libraries(test_path_utils path)
+ cli_test(keyvalue_completion)
setup_datastore_tests()
datastore_test(datastore_access example-schema)
diff --git a/src/ast_handlers.cpp b/src/ast_handlers.cpp
index ca5fe25..8aa6470 100644
--- a/src/ast_handlers.cpp
+++ b/src/ast_handlers.cpp
@@ -1,10 +1,13 @@
#include "ast_handlers.hpp"
-std::set<std::string> generateMissingKeyCompletionSet(std::set<std::string> keysNeeded, std::set<std::string> currentSet)
+std::set<std::string> generateMissingKeyCompletionSet(std::set<std::string> keysNeeded, std::map<std::string, leaf_data_> currentKeys)
{
std::set<std::string> missingKeys;
- std::set_difference(keysNeeded.begin(), keysNeeded.end(),
- currentSet.begin(), currentSet.end(),
- std::inserter(missingKeys, missingKeys.end()));
+
+ for (const auto& key : keysNeeded) {
+ if (currentKeys.find(key) == currentKeys.end()) {
+ missingKeys.insert(key);
+ }
+ }
std::set<std::string> res;
@@ -13,3 +16,12 @@
[] (auto it) { return it + "="; });
return res;
}
+
+std::string leafDataToCompletion(const leaf_data_& value)
+{
+ // Only string-like values need to be quoted
+ if (value.type() == typeid(std::string)) {
+ return escapeListKeyString(leafDataToString(value));
+ }
+ return leafDataToString(value);
+}
diff --git a/src/ast_handlers.hpp b/src/ast_handlers.hpp
index e59fc36..6ecfbc5 100644
--- a/src/ast_handlers.hpp
+++ b/src/ast_handlers.hpp
@@ -30,7 +30,7 @@
_pass(context) = false;
parserContext.m_errorMsg = "Key \"" + ast.first + "\" was entered more than once.";
} else {
- parserContext.m_tmpListKeys.insert(ast.first);
+ parserContext.m_tmpListKeys.insert({ast.first, ast.second});
}
}
@@ -68,11 +68,11 @@
if (schema.listHasKey(location, list, ast)) {
schemaNode_ listNode;
- listNode.m_prefix = parserContext.m_curModule ? boost::optional<module_>{{*parserContext.m_curModule}} : boost::none;
+ listNode.m_prefix = parserContext.m_curModule.flat_map([] (auto mod) { return boost::optional<module_>{{mod}}; });;
listNode.m_suffix = list_{parserContext.m_tmpListName};
location.m_nodes.push_back(listNode);
parserContext.m_tmpListKeyLeafPath.m_location = location;
- parserContext.m_tmpListKeyLeafPath.m_node = {boost::none, ast};
+ parserContext.m_tmpListKeyLeafPath.m_node = { parserContext.m_curModule, ast };
} else {
parserContext.m_errorMsg = parserContext.m_tmpListName + " is not indexed by \"" + ast + "\".";
_pass(context) = false;
@@ -577,7 +577,7 @@
}
};
-std::set<std::string> generateMissingKeyCompletionSet(std::set<std::string> keysNeeded, std::set<std::string> currentSet);
+std::set<std::string> generateMissingKeyCompletionSet(std::set<std::string> keysNeeded, std::map<std::string, leaf_data_> currentSet);
struct createKeySuggestions_class {
template <typename T, typename Iterator, typename Context>
@@ -593,6 +593,42 @@
}
};
+std::string leafDataToCompletion(const leaf_data_& value);
+
+struct createValueSuggestions_class {
+ template <typename T, typename Iterator, typename Context>
+ void on_success(Iterator const& begin, Iterator const&, T&, Context const& context)
+ {
+ auto& parserContext = x3::get<parser_context_tag>(context);
+ if (!parserContext.m_completing) {
+ return;
+ }
+ const auto& dataQuery = parserContext.m_dataquery;
+
+ parserContext.m_completionIterator = begin;
+ auto listInstances = dataQuery->listKeys(parserContext.currentDataPath(), {parserContext.m_curModule, parserContext.m_tmpListName});
+
+ decltype(listInstances) filteredInstances;
+ //This filters out instances, which don't correspond to the partial instance we have.
+ const auto partialFitsComplete = [&parserContext] (const auto& complete) {
+ const auto& partial = parserContext.m_tmpListKeys;
+ return std::all_of(partial.begin(), partial.end(), [&complete] (const auto& oneKV) {
+ const auto& [k, v] = oneKV;
+ return complete.at(k) == v;
+ });
+ };
+ std::copy_if(listInstances.begin(), listInstances.end(), std::inserter(filteredInstances, filteredInstances.end()), partialFitsComplete);
+
+ std::set<std::string> validValues;
+
+ std::transform(filteredInstances.begin(), filteredInstances.end(), std::inserter(validValues, validValues.end()), [&parserContext] (const auto& instance) {
+ return leafDataToCompletion(instance.at(parserContext.m_tmpListKeyLeafPath.m_node.second));
+ });
+
+ parserContext.m_suggestions = validValues;
+ }
+};
+
struct suggestKeysEnd_class {
template <typename T, typename Iterator, typename Context>
void on_success(Iterator const& begin, Iterator const&, T&, Context const& context)
diff --git a/src/grammars.hpp b/src/grammars.hpp
index 98ed8b7..11d1db0 100644
--- a/src/grammars.hpp
+++ b/src/grammars.hpp
@@ -71,6 +71,7 @@
x3::rule<initializePath_class, x3::unused_type> const initializePath = "initializePath";
x3::rule<createPathSuggestions_class, x3::unused_type> const createPathSuggestions = "createPathSuggestions";
x3::rule<createKeySuggestions_class, x3::unused_type> const createKeySuggestions = "createKeySuggestions";
+x3::rule<createValueSuggestions_class, x3::unused_type> const createValueSuggestions = "createValueSuggestions";
x3::rule<suggestKeysEnd_class, x3::unused_type> const suggestKeysEnd = "suggestKeysEnd";
x3::rule<createCommandSuggestions_class, x3::unused_type> const createCommandSuggestions = "createCommandSuggestions";
x3::rule<completing_class, x3::unused_type> const completing = "completing";
@@ -110,11 +111,14 @@
auto const createKeySuggestions_def =
x3::eps;
+auto const createValueSuggestions_def =
+ x3::eps;
+
auto const suggestKeysEnd_def =
x3::eps;
auto const keyValue_def =
- key_identifier > '=' > leaf_data;
+ key_identifier > '=' > createValueSuggestions > leaf_data;
auto const keyValueWrapper =
lexeme['[' > createKeySuggestions > keyValue > suggestKeysEnd > ']'];
@@ -396,6 +400,7 @@
BOOST_SPIRIT_DEFINE(command)
BOOST_SPIRIT_DEFINE(createPathSuggestions)
BOOST_SPIRIT_DEFINE(createKeySuggestions)
+BOOST_SPIRIT_DEFINE(createValueSuggestions)
BOOST_SPIRIT_DEFINE(suggestKeysEnd)
BOOST_SPIRIT_DEFINE(createCommandSuggestions)
BOOST_SPIRIT_DEFINE(completing)
diff --git a/src/main.cpp b/src/main.cpp
index d1b45e0..fd064ba 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -36,7 +36,8 @@
std::cout << "Welcome to netconf-cli" << std::endl;
SysrepoAccess datastore("netconf-cli");
- Parser parser(datastore.schema());
+ auto dataQuery = std::make_shared<DataQuery>(datastore);
+ Parser parser(datastore.schema(), dataQuery);
using replxx::Replxx;
diff --git a/src/parser.cpp b/src/parser.cpp
index c3dc088..ff262fc 100644
--- a/src/parser.cpp
+++ b/src/parser.cpp
@@ -15,8 +15,9 @@
InvalidCommandException::~InvalidCommandException() = default;
-Parser::Parser(const std::shared_ptr<const Schema> schema)
+Parser::Parser(const std::shared_ptr<const Schema> schema, const std::shared_ptr<const DataQuery> dataQuery)
: m_schema(schema)
+ , m_dataquery(dataQuery)
{
m_curDir.m_scope = Scope::Absolute;
}
@@ -29,7 +30,7 @@
command_ Parser::parseCommand(const std::string& line, std::ostream& errorStream)
{
command_ parsedCommand;
- ParserContext ctx(*m_schema, m_curDir);
+ ParserContext ctx(*m_schema, nullptr, m_curDir);
auto it = line.begin();
boost::spirit::x3::error_handler<std::string::const_iterator> errorHandler(it, line.end(), errorStream);
@@ -51,7 +52,7 @@
{
std::set<std::string> completions;
command_ parsedCommand;
- ParserContext ctx(*m_schema, m_curDir);
+ ParserContext ctx(*m_schema, m_dataquery, m_curDir);
ctx.m_completing = true;
auto it = line.begin();
boost::spirit::x3::error_handler<std::string::const_iterator> errorHandler(it, line.end(), errorStream);
diff --git a/src/parser.hpp b/src/parser.hpp
index c2cd8d6..7a94919 100644
--- a/src/parser.hpp
+++ b/src/parser.hpp
@@ -9,6 +9,7 @@
#pragma once
#include "ast_commands.hpp"
#include "ast_path.hpp"
+#include "data_query.hpp"
#include "schema.hpp"
@@ -32,7 +33,7 @@
class Parser {
public:
- Parser(const std::shared_ptr<const Schema> schema);
+ Parser(const std::shared_ptr<const Schema> schema, const std::shared_ptr<const DataQuery> dataQuery = nullptr);
command_ parseCommand(const std::string& line, std::ostream& errorStream);
void changeNode(const dataPath_& name);
std::string currentNode() const;
@@ -41,5 +42,6 @@
private:
const std::shared_ptr<const Schema> m_schema;
+ const std::shared_ptr<const DataQuery> m_dataquery;
dataPath_ m_curDir;
};
diff --git a/src/parser_context.cpp b/src/parser_context.cpp
index 7750301..26e6fa3 100644
--- a/src/parser_context.cpp
+++ b/src/parser_context.cpp
@@ -7,9 +7,10 @@
*/
#include "parser_context.hpp"
-ParserContext::ParserContext(const Schema& schema, const dataPath_& curDir)
+ParserContext::ParserContext(const Schema& schema, const std::shared_ptr<const DataQuery> dataQuery, const dataPath_& curDir)
: m_schema(schema)
, m_curPathOrig(curDir)
+ , m_dataquery(dataQuery)
, m_curPath(curDir)
{
if (!currentDataPath().m_nodes.empty() && currentDataPath().m_nodes.at(0).m_prefix)
diff --git a/src/parser_context.hpp b/src/parser_context.hpp
index 77357e7..98022db 100644
--- a/src/parser_context.hpp
+++ b/src/parser_context.hpp
@@ -6,9 +6,10 @@
*
*/
+#include "data_query.hpp"
#include "schema.hpp"
struct ParserContext {
- ParserContext(const Schema& schema, const dataPath_& curDir);
+ ParserContext(const Schema& schema, const std::shared_ptr<const DataQuery> dataQuery, const dataPath_& curDir);
schemaPath_ currentSchemaPath();
dataPath_ currentDataPath();
void clearPath();
@@ -18,16 +19,17 @@
const Schema& m_schema;
const dataPath_ m_curPathOrig;
+ const std::shared_ptr<const DataQuery> m_dataquery;
boost::optional<std::string> m_curModule;
std::string m_errorMsg;
std::string m_tmpListName;
bool m_topLevelModulePresent = false;
- std::set<std::string> m_tmpListKeys;
struct {
schemaPath_ m_location;
ModuleNodePair m_node;
} m_tmpListKeyLeafPath;
+ std::map<std::string, leaf_data_> m_tmpListKeys;
bool m_errorHandled = false;
bool m_completing = false;
std::set<std::string> m_suggestions;
diff --git a/tests/datastoreaccess_mock.hpp b/tests/datastoreaccess_mock.hpp
new file mode 100644
index 0000000..289d919
--- /dev/null
+++ b/tests/datastoreaccess_mock.hpp
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2020 CESNET, https://photonics.cesnet.cz/
+ *
+ * Written by Václav Kubernát <kubernat@cesnet.cz>
+ *
+*/
+
+#include <map>
+#include "datastore_access.hpp"
+#include "trompeloeil_doctest.h"
+#include "utils.hpp"
+
+namespace trompeloeil {
+ template <>
+ inline void print(std::ostream& os, const leaf_data_& data)
+ {
+ os << leafDataToString(data);
+ }
+}
+
+class MockDatastoreAccess : public trompeloeil::mock_interface<DatastoreAccess> {
+ IMPLEMENT_MOCK1(getItems);
+ IMPLEMENT_MOCK2(setLeaf);
+ IMPLEMENT_MOCK1(createPresenceContainer);
+ IMPLEMENT_MOCK1(deletePresenceContainer);
+ IMPLEMENT_MOCK1(createListInstance);
+ IMPLEMENT_MOCK1(deleteListInstance);
+ IMPLEMENT_MOCK2(executeRpc);
+
+ // Can't use IMPLEMENT_MOCK for private methods - IMPLEMENT_MOCK needs full visibility of the method
+ MAKE_MOCK1(listInstances, std::vector<ListInstance>(const std::string&), override);
+
+
+ IMPLEMENT_MOCK0(schema);
+
+ IMPLEMENT_MOCK0(commitChanges);
+ IMPLEMENT_MOCK0(discardChanges);
+};
+
+
diff --git a/tests/enum_completion.cpp b/tests/enum_completion.cpp
index 29f23a7..c6b31d8 100644
--- a/tests/enum_completion.cpp
+++ b/tests/enum_completion.cpp
@@ -8,6 +8,7 @@
*/
#include "trompeloeil_doctest.h"
+#include "datastoreaccess_mock.hpp"
#include "parser.hpp"
#include "pretty_printers.hpp"
#include "static_schema.hpp"
@@ -22,7 +23,16 @@
schema->addList("/", "mod:list", {"number"});
schema->addLeaf("/mod:list", "mod:number", yang::LeafDataTypes::Int32);
schema->addLeafEnum("/mod:list", "mod:leafInList", {"ano", "anoda", "ne", "katoda"});
- Parser parser(schema);
+ 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.
+ ALLOW_CALL(*mockDatastore, listInstances("/mod:list"))
+ .RETURN(std::vector<ListInstance>{});
+
+ // DataQuery gets the schema from DatastoreAccess once
+ auto expectation = NAMED_REQUIRE_CALL(*mockDatastore, schema())
+ .RETURN(schema);
+ auto dataQuery = std::make_shared<DataQuery>(*mockDatastore);
+ Parser parser(schema, dataQuery);
std::string input;
std::ostringstream errorStream;
diff --git a/tests/keyvalue_completion.cpp b/tests/keyvalue_completion.cpp
new file mode 100644
index 0000000..56c7841
--- /dev/null
+++ b/tests/keyvalue_completion.cpp
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2020 CESNET, https://photonics.cesnet.cz/
+ *
+ * Written by Václav Kubernát <kubervac@fit.cvut.cz>
+ *
+*/
+
+#include <experimental/iterator>
+#include <iostream>
+#include "trompeloeil_doctest.h"
+#include "ast_commands.hpp"
+#include "datastoreaccess_mock.hpp"
+#include "parser.hpp"
+#include "static_schema.hpp"
+
+namespace std {
+std::ostream& operator<<(std::ostream& s, const std::set<std::string> set)
+{
+ s << std::endl << "{";
+ std::copy(set.begin(), set.end(), std::experimental::make_ostream_joiner(s, ", "));
+ s << "}" << std::endl;
+ return s;
+}
+}
+
+TEST_CASE("keyvalue_completion")
+{
+ auto schema = std::make_shared<StaticSchema>();
+ schema->addModule("example");
+ schema->addContainer("/", "example:a");
+ schema->addContainer("/", "example:b");
+ schema->addList("/", "example:list", {"number"});
+ schema->addLeaf("/example:list", "example:number", yang::LeafDataTypes::Int32);
+ schema->addList("/", "example:twoKeyList", {"number", "name"});
+ schema->addLeaf("/example:twoKeyList", "example:number", yang::LeafDataTypes::Int32);
+ schema->addLeaf("/example:twoKeyList", "example:name", yang::LeafDataTypes::String);
+ auto mockDatastore = std::make_shared<MockDatastoreAccess>();
+
+ // DataQuery gets the schema from DatastoreAccess once
+ auto expectation = NAMED_REQUIRE_CALL(*mockDatastore, schema())
+ .RETURN(schema);
+ auto dataQuery = std::make_shared<DataQuery>(*mockDatastore);
+ expectation.reset();
+ Parser parser(schema, dataQuery);
+ std::string input;
+ std::ostringstream errorStream;
+
+ std::set<std::string> expected;
+ std::vector<std::shared_ptr<trompeloeil::expectation>> queryExpectations;
+ std::vector<ListInstance> queryReturn;
+
+ SECTION("get example:list[number=")
+ {
+ input = "get example:list[number=";
+ queryReturn = {
+ {{"number", 1}},
+ {{"number", 7}},
+ {{"number", 9}},
+ {{"number", 42}}
+ };
+ expected = {
+ "1",
+ "7",
+ "9",
+ "42"
+ };
+ queryExpectations.push_back(NAMED_REQUIRE_CALL(*mockDatastore, listInstances("/example:list"))
+ .RETURN(queryReturn));
+ }
+
+ SECTION("get example:twoKeyList[number=")
+ {
+ queryReturn = {
+ {{"number", 1}, {"name", std::string{"Petr"}}},
+ {{"number", 7}, {"name", std::string{"Petr"}}},
+ {{"number", 10}, {"name", std::string{"Petr"}}},
+ {{"number", 10}, {"name", std::string{"Honza"}}},
+ {{"number", 100}, {"name", std::string{"Honza"}}},
+ };
+ input = "get example:twoKeyList[";
+ SECTION("no keys set")
+ {
+ SECTION("number")
+ {
+ input += "number=";
+ expected = { "1", "7", "10", "100" };
+ }
+ SECTION("name")
+ {
+ input += "name=";
+ expected = { "'Petr'", "'Honza'"};
+ }
+ queryExpectations.push_back(NAMED_REQUIRE_CALL(*mockDatastore, listInstances("/example:twoKeyList"))
+ .RETURN(queryReturn));
+ }
+
+ SECTION("name is set")
+ {
+ input += "name=";
+ SECTION("Petr")
+ {
+ input += "'Petr'";
+ expected = { "1", "7", "10"};
+ }
+ SECTION("Honza")
+ {
+ input += "'Honza'";
+ expected = { "10", "100" };
+ }
+ input += "][number=";
+ queryExpectations.push_back(NAMED_REQUIRE_CALL(*mockDatastore, listInstances("/example:twoKeyList"))
+ .TIMES(2)
+ .RETURN(queryReturn));
+ }
+
+ SECTION("number is set")
+ {
+ input += "number=";
+ SECTION("1")
+ {
+ input += "1";
+ expected = { "'Petr'" };
+ }
+ SECTION("7")
+ {
+ input += "7";
+ expected = { "'Petr'" };
+ }
+ SECTION("10")
+ {
+ input += "10";
+ expected = { "'Honza'", "'Petr'" };
+ }
+ SECTION("100")
+ {
+ input += "100";
+ expected = { "'Honza'" };
+ }
+ input += "][name=";
+ queryExpectations.push_back(NAMED_REQUIRE_CALL(*mockDatastore, listInstances("/example:twoKeyList"))
+ .TIMES(2)
+ .RETURN(queryReturn));
+ }
+
+ SECTION("both keys are set")
+ {
+ SECTION("get example:twoKeyList[number=123][name='Petr'")
+ {
+ input = "get example:twoKeyList[number=123][name='Petr'";
+ expected = {"]/"};
+ }
+ SECTION("get example:twoKeyList[number=123][name='Petr']")
+ {
+ input = "get example:twoKeyList[number=123][name='Petr']";
+ expected = {"]/"};
+ }
+ SECTION("get example:twoKeyList[number=123][name='Petr'][")
+ {
+ input = "get example:twoKeyList[number=123][name='Petr'][";
+ }
+ // I use ALLOW_CALL here, all this stuff calls it different number of times
+ queryExpectations.push_back(NAMED_ALLOW_CALL(*mockDatastore, listInstances("/example:twoKeyList"))
+ .RETURN(queryReturn));
+ }
+
+ }
+
+ REQUIRE(parser.completeCommand(input, errorStream).m_completions == expected);
+ for (auto& it : queryExpectations) {
+ it.reset();
+ }
+}
diff --git a/tests/path_completion.cpp b/tests/path_completion.cpp
index 832110d..0ad6089 100644
--- a/tests/path_completion.cpp
+++ b/tests/path_completion.cpp
@@ -7,6 +7,7 @@
*/
#include "trompeloeil_doctest.h"
+#include "datastoreaccess_mock.hpp"
#include "parser.hpp"
#include "pretty_printers.hpp"
#include "static_schema.hpp"
@@ -36,7 +37,20 @@
schema->addLeaf("/example:twoKeyList", "example:name", yang::LeafDataTypes::String);
schema->addLeaf("/example:twoKeyList", "example:number", yang::LeafDataTypes::Int32);
schema->addLeaf("/", "example:leafInt", yang::LeafDataTypes::Int32);
- Parser parser(schema);
+ 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.
+ ALLOW_CALL(*mockDatastore, listInstances("/example:list"))
+ .RETURN(std::vector<ListInstance>{});
+ ALLOW_CALL(*mockDatastore, listInstances("/example:twoKeyList"))
+ .RETURN(std::vector<ListInstance>{});
+
+ // DataQuery gets the schema from DatastoreAccess once
+ auto expectation = NAMED_REQUIRE_CALL(*mockDatastore, schema())
+ .RETURN(schema);
+ auto dataQuery = std::make_shared<DataQuery>(*mockDatastore);
+ expectation.reset();
+ Parser parser(schema, dataQuery);
std::string input;
std::ostringstream errorStream;
@@ -188,8 +202,8 @@
SECTION("cd example:list[number=")
{
input = "cd example:list[number=";
- expectedCompletions = {"number="};
- expectedContextLength = 7;
+ expectedCompletions = {};
+ expectedContextLength = 0;
}
SECTION("cd example:list[number=12")