Implement DataQuery

For now, the DataQuery class includes the listKeys method, which will be
used in the parser for tab-completion of values of keys.

The listKeys method uses similar arguments as many methods from the
Schema interface do. This is due to how parsing works: when I parse
lists I won't get the full list instance until all keys are typed.
That's why I can't get the full path to the list: it can't be a full
data path until all keys are known.

I tried many different return types of the method. The first iteration
used just a string to string map. That didn't work well, because
discarding type info meant that I needed to quote strings in THIS
method. And that didn't go well when trying to integrate this to the
parser, because I couldn't compare quoted strings with unquoted strings.

The next return type I tried - a set of string to leaf_data_ maps - was
better. It doesn't discard type info and that makes it possible to
compare in the parser. However, I didn't really make use of an ordered
container, so I changed the type to a vector.

The various implementations of this method were pretty straightforward.
They usually just retrieved all the instances from the datastore with
DatastoreAccess::getItems and then used some sort of an algorithm to
transform the instances into the desired return type. However, there was
a problem with this approach: it is difficult to get ONLY the instances,
and no other data. This means that it is unsuitable to use getItems for this, because:
a) it always works recursively
b) even if it didn't Netconf would still give me more data than I
wanted.

It is possible to create an XML filter, so that no data has to be
discarded, but it's only possible in Netconf and not sysrepo. This
difference is going to be (hopefully) removed with sysrepo2. I decided
to implement a datastore-specific method, where both sysrepo and netconf
use their own best implementation.

Change-Id: I2c978762d3a78286b4f7fb97e16118772ddeb7bc
diff --git a/tests/data_query.cpp b/tests/data_query.cpp
new file mode 100644
index 0000000..c62c240
--- /dev/null
+++ b/tests/data_query.cpp
@@ -0,0 +1,256 @@
+/*
+ * Copyright (C) 2019 CESNET, https://photonics.cesnet.cz/
+ *
+ * Written by Václav Kubernát <kubervac@fit.cvut.cz>
+ *
+*/
+
+#include <experimental/iterator>
+#include "trompeloeil_doctest.h"
+
+#ifdef sysrepo_BACKEND
+#include "sysrepo_access.hpp"
+#elif defined(netconf_BACKEND)
+#include "netconf_access.hpp"
+#include "netopeer_vars.hpp"
+#else
+#error "Unknown backend"
+#endif
+#include "data_query.hpp"
+#include "sysrepo_subscription.hpp"
+#include "utils.hpp"
+
+namespace std {
+std::ostream& operator<<(std::ostream& s, const std::vector<std::map<std::string, leaf_data_>> set)
+{
+    s << std::endl << "{" << std::endl;
+    std::transform(set.begin(), set.end(), std::experimental::make_ostream_joiner(s, ", \n"), [](const auto& map) {
+        std::ostringstream ss;
+        ss << "    {" << std::endl << "        ";
+        std::transform(map.begin(), map.end(), std::experimental::make_ostream_joiner(ss, ", \n        "), [](const auto& keyValue){
+            return "{" + keyValue.first + "{" + boost::core::demangle(keyValue.second.type().name()) + "}" + ", " + leafDataToString(keyValue.second) + "}";
+        });
+        ss << std::endl << "    }";
+        return ss.str();
+    });
+    s << std::endl << "}" << std::endl;
+    return s;
+}
+}
+
+TEST_CASE("data query")
+{
+    trompeloeil::sequence seq1;
+    SysrepoSubscription subscriptionExample("example-schema");
+    SysrepoSubscription subscriptionOther("other-module");
+
+#ifdef sysrepo_BACKEND
+    SysrepoAccess datastore("netconf-cli-test");
+#elif defined(netconf_BACKEND)
+    NetconfAccess datastore(NETOPEER_SOCKET_PATH);
+#else
+#error "Unknown backend"
+#endif
+
+    DataQuery dataquery(datastore);
+
+    SECTION("listKeys")
+    {
+        dataPath_ location;
+        location.m_scope = Scope::Absolute;
+        ModuleNodePair node;
+        std::vector<std::map<std::string, leaf_data_>> expected;
+
+        SECTION("example-schema:person")
+        {
+            datastore.createListInstance("/example-schema:person[name='Vaclav']");
+            datastore.createListInstance("/example-schema:person[name='Tomas']");
+            datastore.createListInstance("/example-schema:person[name='Jan Novak']");
+            node.first = "example-schema";
+            node.second = "person";
+            expected = {
+                {{"name", std::string{"Jan Novak"}}},
+                {{"name", std::string{"Tomas"}}},
+                {{"name", std::string{"Vaclav"}}}
+            };
+        }
+
+        SECTION("example-schema:selectedNumbers")
+        {
+            datastore.createListInstance("/example-schema:selectedNumbers[value='45']");
+            datastore.createListInstance("/example-schema:selectedNumbers[value='99']");
+            datastore.createListInstance("/example-schema:selectedNumbers[value='127']");
+            node.first = "example-schema";
+            node.second = "selectedNumbers";
+            expected = {
+                {{"value", int8_t{127}}},
+                {{"value", int8_t{45}}},
+                {{"value", int8_t{99}}}
+            };
+        }
+
+        SECTION("example-schema:animalWithColor")
+        {
+            datastore.createListInstance("/example-schema:animalWithColor[name='Dog'][color='brown']");
+            datastore.createListInstance("/example-schema:animalWithColor[name='Dog'][color='white']");
+            datastore.createListInstance("/example-schema:animalWithColor[name='Cat'][color='grey']");
+            node.first = "example-schema";
+            node.second = "animalWithColor";
+            expected = {
+                {{"name", std::string{"Cat"}}, {"color", std::string{"grey"}}},
+                {{"name", std::string{"Dog"}}, {"color", std::string{"brown"}}},
+                {{"name", std::string{"Dog"}}, {"color", std::string{"white"}}}
+            };
+        }
+
+        SECTION("example-schema:animalWithColor - quotes in values")
+        {
+            datastore.createListInstance("/example-schema:animalWithColor[name='D\"o\"g'][color=\"b'r'own\"]");
+            node.first = "example-schema";
+            node.second = "animalWithColor";
+            expected = {
+                {{"name", std::string{"D\"o\"g"}}, {"color", std::string{"b'r'own"}}}
+            };
+        }
+
+        SECTION("example-schema:ports")
+        {
+            datastore.createListInstance("/example-schema:ports[name='A']");
+            datastore.createListInstance("/example-schema:ports[name='B']");
+            datastore.createListInstance("/example-schema:ports[name='E']");
+            node.first = "example-schema";
+            node.second = "ports";
+            expected = {
+                {{"name", enum_{"A"}}},
+                {{"name", enum_{"B"}}},
+                {{"name", enum_{"E"}}},
+            };
+        }
+
+        SECTION("example-schema:org/example:people - nested list")
+        {
+            datastore.createListInstance("/example-schema:org[department='accounting']");
+            datastore.createListInstance("/example-schema:org[department='sales']");
+            datastore.createListInstance("/example-schema:org[department='programmers']");
+            datastore.createListInstance("/example-schema:org[department='accounting']/people[name='Alice']");
+            datastore.createListInstance("/example-schema:org[department='accounting']/people[name='Bob']");
+            datastore.createListInstance("/example-schema:org[department='sales']/people[name='Alice']");
+            datastore.createListInstance("/example-schema:org[department='sales']/people[name='Cyril']");
+            datastore.createListInstance("/example-schema:org[department='sales']/people[name='Alice']/computers[type='laptop']");
+            datastore.createListInstance("/example-schema:org[department='sales']/people[name='Alice']/computers[type='server']");
+            datastore.createListInstance("/example-schema:org[department='sales']/people[name='Cyril']/computers[type='PC']");
+            datastore.createListInstance("/example-schema:org[department='sales']/people[name='Cyril']/computers[type='server']");
+
+            SECTION("outer list")
+            {
+                node.first = "example-schema";
+                node.second = "org";
+                expected = {
+                    {{"department", std::string{"accounting"}}},
+                    {{"department", std::string{"sales"}}},
+                    {{"department", std::string{"programmers"}}},
+                };
+            }
+
+            SECTION("nested list")
+            {
+                listElement_ list;
+                list.m_name = "org";
+                node.second = "people";
+                SECTION("accounting department")
+                {
+                    list.m_keys = {
+                        {"department", std::string{"accounting"}}
+                    };
+                    expected = {
+                        {{"name", std::string{"Alice"}}},
+                        {{"name", std::string{"Bob"}}},
+                    };
+                }
+                SECTION("sales department")
+                {
+                    list.m_keys = {
+                        {"department", std::string{"sales"}}
+                    };
+                    expected = {
+                        {{"name", std::string{"Alice"}}},
+                        {{"name", std::string{"Cyril"}}},
+                    };
+                }
+                SECTION("programmers department")
+                {
+                    list.m_keys = {
+                        {"department", std::string{"programmers"}}
+                    };
+                    expected = {
+                    };
+                }
+                location.m_nodes.push_back(dataNode_{{"example-schema"}, list});
+            }
+
+            SECTION("THREE MF NESTED LISTS")
+            {
+                listElement_ listOrg;
+                listOrg.m_name = "org";
+                listOrg.m_keys = {
+                    {"department", std::string{"sales"}}
+                };
+
+                listElement_ listPeople;
+                node.second = "computers";
+
+                SECTION("alice computers")
+                {
+                    listPeople.m_name = "people";
+                    listPeople.m_keys = {
+                        {"name", std::string{"Alice"}}
+                    };
+                    expected = {
+                        {{"type", enum_{"laptop"}}},
+                        {{"type", enum_{"server"}}},
+                    };
+
+                }
+
+                SECTION("cyril computers")
+                {
+                    listPeople.m_name = "people";
+                    listPeople.m_keys = {
+                        {"name", std::string{"Cyril"}}
+                    };
+                    expected = {
+                        {{"type", enum_{"PC"}}},
+                        {{"type", enum_{"server"}}},
+                    };
+                }
+
+                location.m_nodes.push_back(dataNode_{{"example-schema"}, listOrg});
+                location.m_nodes.push_back(dataNode_{listPeople});
+
+            }
+        }
+
+        SECTION("/other-module:parking-lot/example-schema:cars - list coming from an augment")
+        {
+            datastore.createListInstance("/other-module:parking-lot/example-schema:cars[id='1']");
+            datastore.createListInstance("/other-module:parking-lot/example-schema:cars[id='2']");
+
+            location.m_nodes.push_back(dataNode_{{"other-module"}, container_{"parking-lot"}});
+            node.first = "example-schema";
+            node.second = "cars";
+            expected = {
+                {{"id", int32_t{1}}},
+                {{"id", int32_t{2}}},
+            };
+
+        }
+
+        datastore.commitChanges();
+        std::sort(expected.begin(), expected.end());
+        auto keys = dataquery.listKeys(location, node);
+        std::sort(keys.begin(), keys.end());
+        REQUIRE(keys == expected);
+    }
+
+    waitForCompletionAndBitMore(seq1);
+}