Implement YangSchema subclass

While we do not necessarily want to use `-isystem` for libyang, we
definitely need to use it for Boost itself. We are only using SPirit X3
directly, but that one still brings in a great deal of other supporting
libraries, including variant and a metaprogramming library.

In our CI environment, these are installed into the same prefix. Boost
itself is OK as we're including it via cmake imported target which uses
-isystem behind the scene. However, if we then add libyang via a regular
target_include_directories, the same path gets pushed into the include
paths, this time without -isystem, but rather as a regular -I. This in
turn starts affecting Boost, and we're screwed.

Fix this by ensuring that we consider libyang to be a system library,
too. We won't get annoyed by some extra warnings and therefore we won't
be able to improve that library as much as the CI would force us
otherwise :).

Change-Id: I7ec9b28501a46f8b2219f4920cbf5c1e4df59d7e
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 5928c0e..4e09b90 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -63,6 +63,14 @@
     )
 target_link_libraries(schemas PUBLIC Boost::boost)
 
+add_library(yangschema STATIC
+    src/yang_schema.cpp
+    )
+target_link_libraries(yangschema ${LIBYANG_LIBRARIES})
+# Ensure that this doesn't override Boost's -isystem -- see the log for details.
+target_include_directories(yangschema SYSTEM PRIVATE ${LIBYANG_INCLUDEDIR})
+link_directories(${LIBYANG_LIBRARY_DIRS})
+
 add_library(parser STATIC
     src/parser.cpp
     src/ast_commands.cpp
@@ -76,7 +84,7 @@
 add_executable(netconf-cli
     src/main.cpp
     )
-target_link_libraries(netconf-cli docopt parser)
+target_link_libraries(netconf-cli yangschema docopt parser)
 add_dependencies(netconf-cli target-NETCONF_CLI_VERSION)
 target_include_directories(netconf-cli PRIVATE ${PROJECT_BINARY_DIR})
 
@@ -118,6 +126,8 @@
     cli_test(ls)
     cli_test(presence_containers)
     cli_test(leaf_editing)
+    cli_test(yang)
+    target_link_libraries(test_yang yangschema)
 
 endif()
 
diff --git a/src/yang_schema.cpp b/src/yang_schema.cpp
new file mode 100644
index 0000000..8cccf23
--- /dev/null
+++ b/src/yang_schema.cpp
@@ -0,0 +1,225 @@
+/*
+ * Copyright (C) 2018 CESNET, https://photonics.cesnet.cz/
+ * Copyright (C) 2018 FIT CVUT, https://fit.cvut.cz/
+ *
+ * Written by Václav Kubernát <kubervac@fit.cvut.cz>
+ *
+*/
+
+#include <libyang/Libyang.hpp>
+#include <libyang/Tree_Schema.hpp>
+#include <string_view>
+#include "utils.hpp"
+#include "yang_schema.hpp"
+
+class YangLoadError : public std::runtime_error {
+public:
+    using std::runtime_error::runtime_error;
+    ~YangLoadError() override = default;
+};
+
+class UnsupportedYangTypeException : public std::runtime_error {
+public:
+    using std::runtime_error::runtime_error;
+    ~UnsupportedYangTypeException() override = default;
+};
+
+class InvalidSchemaQueryException : public std::runtime_error {
+public:
+    using std::runtime_error::runtime_error;
+    ~InvalidSchemaQueryException() override = default;
+};
+
+std::string pathToYangAbsSchemPath(const path_& path)
+{
+    std::string res = "/";
+    std::string currentModule;
+
+    for (const auto& it : path.m_nodes) {
+        const auto name = nodeToSchemaString(it);
+
+        if (it.m_suffix.type() == typeid(module_)) {
+            currentModule = name;
+            continue;
+        } else {
+            res += currentModule + ":";
+            res += name + "/";
+        }
+    }
+
+    return res;
+}
+
+YangSchema::YangSchema()
+{
+    m_context = std::make_shared<Context>();
+}
+
+YangSchema::~YangSchema() = default;
+
+void YangSchema::addSchemaString(const char* schema)
+{
+    if (!m_context->parse_module_mem(schema, LYS_IN_YANG)) {
+        throw YangLoadError("Couldn't load schema");
+    }
+}
+
+void YangSchema::addSchemaDirectory(const char* directoryName)
+{
+    if (m_context->set_searchdir(directoryName)) {
+        throw YangLoadError("Couldn't add schema search directory");
+    }
+}
+
+void YangSchema::addSchemaFile(const char* filename)
+{
+    if (!m_context->parse_module_path(filename, LYS_IN_YANG)) {
+        throw YangLoadError("Couldn't load schema");
+    }
+}
+
+bool YangSchema::isModule(const path_&, const std::string& name) const
+{
+    const auto set = modules();
+    return set.find(name) != set.end();
+}
+
+bool YangSchema::isContainer(const path_& location, const ModuleNodePair& node) const
+{
+    const auto schemaNode = getSchemaNode(location, node);
+    return schemaNode && schemaNode->nodetype() == LYS_CONTAINER;
+}
+
+bool YangSchema::isLeaf(const path_& location, const ModuleNodePair& node) const
+{
+    const auto schemaNode = getSchemaNode(location, node);
+    return schemaNode && schemaNode->nodetype() == LYS_LEAF;
+}
+
+bool YangSchema::isList(const path_& location, const ModuleNodePair& node) const
+{
+    const auto schemaNode = getSchemaNode(location, node);
+    return schemaNode && schemaNode->nodetype() == LYS_LIST;
+}
+
+bool YangSchema::isPresenceContainer(const path_& location, const ModuleNodePair& node) const
+{
+    if (!isContainer(location, node))
+        return false;
+    return Schema_Node_Container(getSchemaNode(location, node)).presence();
+}
+
+bool YangSchema::leafEnumHasValue(const path_& location, const ModuleNodePair& node, const std::string& value) const
+{
+    if (!isLeaf(location, node) || leafType(location, node) != yang::LeafDataTypes::Enum)
+        return false;
+
+    Schema_Node_Leaf leaf(getSchemaNode(location, node));
+    const auto enm = std::unique_ptr<std::vector<S_Type_Enum>>(leaf.type()->info()->enums()->enm());
+
+    return std::any_of(enm->begin(), enm->end(), [=](const auto& x) { return x->name() == value; });
+}
+
+bool YangSchema::listHasKey(const path_& location, const ModuleNodePair& node, const std::string& key) const
+{
+    if (!isList(location, node))
+        return false;
+    const auto keys = listKeys(location, node);
+    return keys.find(key) != keys.end();
+}
+
+bool YangSchema::nodeExists(const std::string& location, const std::string& node) const
+{
+    const auto absPath = location + "/" + node;
+    const auto set = m_context->find_path(absPath.c_str());
+    return set->number() == 1;
+}
+
+S_Set YangSchema::getNodeSet(const path_& location, const ModuleNodePair& node) const
+{
+    std::string absPath = location.m_nodes.empty() ? "" : "/";
+    absPath += pathToAbsoluteSchemaString(location) + "/" + fullNodeName(location, node);
+    return m_context->find_path(absPath.c_str());
+}
+
+std::shared_ptr<Schema_Node> YangSchema::getSchemaNode(const path_& location, const ModuleNodePair& node) const
+{
+    const auto set = getNodeSet(location, node);
+    if (!set)
+        return nullptr;
+    const auto schemaSet = std::unique_ptr<std::vector<std::shared_ptr<Schema_Node>>>(set->schema());
+    if (set->number() != 1)
+        return nullptr;
+    return (*(schemaSet->begin()));
+}
+
+const std::set<std::string> YangSchema::listKeys(const path_& location, const ModuleNodePair& node) const
+{
+    std::set<std::string> keys;
+    if (!isList(location, node))
+        return keys;
+    Schema_Node_List list(getSchemaNode(location, node));
+    const auto keysVec = std::unique_ptr<std::vector<S_Schema_Node_Leaf>>(list.keys());
+
+    std::transform(keysVec->begin(), keysVec->end(), std::inserter(keys, keys.begin()),
+            [] (const auto& it) {return it->name();});
+    return keys;
+}
+
+yang::LeafDataTypes YangSchema::leafType(const path_& location, const ModuleNodePair& node) const
+{
+    using namespace std::string_literals;
+    if (!isLeaf(location, node))
+        throw InvalidSchemaQueryException(fullNodeName(location, node) + " is not a leaf");
+
+    Schema_Node_Leaf leaf(getSchemaNode(location, node));
+    switch (leaf.type()->base()) {
+    case LY_TYPE_STRING:
+        return yang::LeafDataTypes::String;
+    case LY_TYPE_DEC64:
+        return yang::LeafDataTypes::Decimal;
+    case LY_TYPE_BOOL:
+        return yang::LeafDataTypes::Bool;
+    case LY_TYPE_INT32:
+        return yang::LeafDataTypes::Int;
+    case LY_TYPE_UINT32:
+        return yang::LeafDataTypes::Uint;
+    case LY_TYPE_ENUM:
+        return yang::LeafDataTypes::Enum;
+    default:
+        throw UnsupportedYangTypeException("the type of "s + fullNodeName(location, node) + " is not supported");
+    }
+}
+
+std::set<std::string> YangSchema::modules() const
+{
+    const auto modules = std::unique_ptr<std::vector<S_Module>>(m_context->get_module_iter());
+
+    std::set<std::string> res;
+    std::transform(modules->begin(), modules->end(),
+                   std::inserter(res, res.end()),
+                   [] (const auto module) { return module->name(); });
+    return res;
+}
+
+std::set<std::string> YangSchema::childNodes(const path_& path) const
+{
+    using namespace std::string_view_literals;
+    std::set<std::string> res;
+    if (path.m_nodes.empty()) {
+        const auto nodeVec = std::unique_ptr<std::vector<S_Schema_Node>>(m_context->data_instantiables(0));
+        for (const auto it : *nodeVec) {
+            if (it->module()->name() == "ietf-yang-library"sv)
+                continue;
+            res.insert(std::string(it->module()->name()) + ":" + it->name());
+        }
+    } else {
+        const auto absolutePath = "/" + pathToAbsoluteSchemaString(path);
+        const auto set = m_context->find_path(absolutePath.c_str());
+        const auto schemaSet = std::unique_ptr<std::vector<std::shared_ptr<Schema_Node>>>(set->schema());
+        for (auto it = (*(schemaSet->begin()))->child(); it; it = it->next()) {
+            res.insert(std::string(it->module()->name()) + ":" + it->name());
+        }
+    }
+    return res;
+}
diff --git a/src/yang_schema.hpp b/src/yang_schema.hpp
new file mode 100644
index 0000000..e30c701
--- /dev/null
+++ b/src/yang_schema.hpp
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2018 CESNET, https://photonics.cesnet.cz/
+ * Copyright (C) 2018 FIT CVUT, https://fit.cvut.cz/
+ *
+ * Written by Václav Kubernát <kubervac@fit.cvut.cz>
+ *
+*/
+
+#pragma once
+
+#include <set>
+#include <stdexcept>
+#include <unordered_map>
+#include "ast_path.hpp"
+#include "schema.hpp"
+
+class Context;
+class Set;
+class Schema_Node;
+
+/*! \class YangSchema
+ *     \brief A schema class, which uses libyang for queries.
+ *         */
+class YangSchema : public Schema {
+public:
+    YangSchema();
+    ~YangSchema() override;
+
+    bool isContainer(const path_& location, const ModuleNodePair& node) const override;
+    bool isLeaf(const path_& location, const ModuleNodePair& node) const override;
+    bool isModule(const path_& location, const std::string& name) const override;
+    bool isList(const path_& location, const ModuleNodePair& node) const override;
+    bool isPresenceContainer(const path_& location, const ModuleNodePair& node) const override;
+    bool leafEnumHasValue(const path_& location, const ModuleNodePair& node, const std::string& value) const override;
+    bool listHasKey(const path_& location, const ModuleNodePair& node, const std::string& key) const override;
+    bool nodeExists(const std::string& location, const std::string& node) const override;
+    const std::set<std::string> listKeys(const path_& location, const ModuleNodePair& node) const override;
+    yang::LeafDataTypes leafType(const path_& location, const ModuleNodePair& node) const override;
+    std::set<std::string> childNodes(const path_& path) const override;
+
+    /** @short Adds a new module passed as a YANG string. */
+    void addSchemaString(const char* schema);
+
+    /** @short Adds a new module from a file. */
+    void addSchemaFile(const char* filename);
+
+    /** @short Adds a new directory for schema lookup. */
+    void addSchemaDirectory(const char* directoryName);
+
+private:
+    std::set<std::string> modules() const;
+    bool nodeExists(const path_& location, const ModuleNodePair& node) const;
+
+    /** @short Returns a set of nodes, that match the location and name criteria. */
+    std::shared_ptr<Set> getNodeSet(const path_& location, const ModuleNodePair& node) const;
+
+    /** @short Returns a single Schema_Node if the criteria matches only one, otherwise nullptr. */
+    std::shared_ptr<Schema_Node> getSchemaNode(const path_& location, const ModuleNodePair& node) const;
+    std::shared_ptr<Context> m_context;
+};
diff --git a/tests/yang.cpp b/tests/yang.cpp
new file mode 100644
index 0000000..09bef03
--- /dev/null
+++ b/tests/yang.cpp
@@ -0,0 +1,356 @@
+/*
+ * Copyright (C) 2018 CESNET, https://photonics.cesnet.cz/
+ * Copyright (C) 2018 FIT CVUT, https://fit.cvut.cz/
+ *
+ * Written by Václav Kubernát <kubervac@fit.cvut.cz>
+ *
+*/
+
+#include "trompeloeil_catch.h"
+#include "yang_schema.hpp"
+
+const char* schema = R"(
+module example-schema {
+    namespace "http://example.com/example-sports";
+    prefix coze;
+
+    container a {
+        container a2 {
+            container a3 {
+                presence true;
+            }
+        }
+
+        leaf leafa {
+            type string;
+        }
+    }
+
+    container b {
+        container b2 {
+            presence true;
+            container b3 {
+            }
+        }
+    }
+
+    leaf leafString {
+        type string;
+    }
+
+    leaf leafDecimal {
+        type decimal64 {
+            fraction-digits 5;
+        }
+    }
+
+    leaf leafBool {
+        type boolean;
+    }
+
+    leaf leafInt {
+        type int32;
+    }
+
+    leaf leafUint {
+        type uint32;
+    }
+
+    leaf leafEnum {
+        type enumeration {
+            enum lol;
+            enum data;
+            enum coze;
+        }
+    }
+
+    list _list {
+        key number;
+
+        leaf number {
+            type int32;
+        }
+
+        container contInList {
+            presence true;
+        }
+    }
+
+    list twoKeyList {
+        key "name number";
+
+        leaf number {
+            type int32;
+        }
+
+        leaf name {
+            type string;
+        }
+    }
+})";
+
+TEST_CASE("yangschema")
+{
+    YangSchema ys;
+    ys.addSchemaString(schema);
+    path_ path;
+    ModuleNodePair node;
+
+    SECTION("positive")
+    {
+        SECTION("isContainer")
+        {
+            SECTION("example-schema:a")
+            {
+                node.first = "example-schema";
+                node.second = "a";
+            }
+
+            SECTION("example-schema:a/a2")
+            {
+                path.m_nodes.push_back(node_(module_{"example-schema"}, container_("a")));
+                node.second = "a2";
+            }
+
+            REQUIRE(ys.isContainer(path, node));
+        }
+        SECTION("isLeaf")
+        {
+            SECTION("example-schema:leafString")
+            {
+                node.first = "example-schema";
+                node.second = "leafString";
+            }
+
+            SECTION("example-schema:a/leafa")
+            {
+                path.m_nodes.push_back(node_(module_{"example-schema"}, container_("a")));
+                node.first = "example-schema";
+                node.second = "leafa";
+            }
+
+            REQUIRE(ys.isLeaf(path, node));
+        }
+        SECTION("isModule")
+        {
+            REQUIRE(ys.isModule(path, "example-schema"));
+        }
+        SECTION("isList")
+        {
+            SECTION("example-schema:_list")
+            {
+                node.first = "example-schema";
+                node.second = "_list";
+            }
+
+            SECTION("example-schema:twoKeyList")
+            {
+                node.first = "example-schema";
+                node.second = "twoKeyList";
+            }
+
+            REQUIRE(ys.isList(path, node));
+        }
+        SECTION("isPresenceContainer")
+        {
+            SECTION("example-schema:a/a2/a3")
+            {
+                path.m_nodes.push_back(node_(module_{"example-schema"}, container_("a")));
+                path.m_nodes.push_back(node_(module_{"example-schema"}, container_("a2")));
+                node.second = "a3";
+            }
+
+            REQUIRE(ys.isPresenceContainer(path, node));
+        }
+        SECTION("leafEnumHasValue")
+        {
+            node.first = "example-schema";
+            node.second = "leafEnum";
+            std::string value;
+
+            SECTION("lol")
+            value = "lol";
+
+            SECTION("data")
+            value = "data";
+
+            SECTION("coze")
+            value = "coze";
+
+            REQUIRE(ys.leafEnumHasValue(path, node, value));
+        }
+        SECTION("listHasKey")
+        {
+            std::string key;
+
+            SECTION("_list")
+            {
+                node.first = "example-schema";
+                node.second = "_list";
+                SECTION("number")
+                key = "number";
+            }
+
+            SECTION("twoKeyList")
+            {
+                node.first = "example-schema";
+                node.second = "twoKeyList";
+                SECTION("number")
+                key = "number";
+                SECTION("name")
+                key = "name";
+            }
+
+            REQUIRE(ys.listHasKey(path, node, key));
+        }
+        SECTION("listKeys")
+        {
+            std::set<std::string> set;
+
+            SECTION("_list")
+            {
+                set = {"number"};
+                node.first = "example-schema";
+                node.second = "_list";
+            }
+
+            SECTION("twoKeyList")
+            {
+                set = {"number", "name"};
+                node.first = "example-schema";
+                node.second = "twoKeyList";
+            }
+
+            REQUIRE(ys.listKeys(path, node) == set);
+        }
+        SECTION("leafType")
+        {
+            yang::LeafDataTypes type;
+
+            SECTION("leafString")
+            {
+                node.first = "example-schema";
+                node.second = "leafString";
+                type = yang::LeafDataTypes::String;
+            }
+
+            SECTION("leafDecimal")
+            {
+                node.first = "example-schema";
+                node.second = "leafDecimal";
+                type = yang::LeafDataTypes::Decimal;
+            }
+
+            SECTION("leafBool")
+            {
+                node.first = "example-schema";
+                node.second = "leafBool";
+                type = yang::LeafDataTypes::Bool;
+            }
+
+            SECTION("leafInt")
+            {
+                node.first = "example-schema";
+                node.second = "leafInt";
+                type = yang::LeafDataTypes::Int;
+            }
+
+            SECTION("leafUint")
+            {
+                node.first = "example-schema";
+                node.second = "leafUint";
+                type = yang::LeafDataTypes::Uint;
+            }
+
+            SECTION("leafEnum")
+            {
+                node.first = "example-schema";
+                node.second = "leafEnum";
+                type = yang::LeafDataTypes::Enum;
+            }
+
+            REQUIRE(ys.leafType(path, node) == type);
+        }
+        SECTION("childNodes")
+        {
+            std::set<std::string> set;
+
+            SECTION("<root>")
+            {
+                set = {"example-schema:a", "example-schema:b", "example-schema:leafString",
+                       "example-schema:leafDecimal", "example-schema:leafBool", "example-schema:leafInt",
+                       "example-schema:leafUint", "example-schema:leafEnum", "example-schema:_list", "example-schema:twoKeyList"};
+            }
+
+            SECTION("a")
+            {
+                path.m_nodes.push_back(node_(module_{"example-schema"}, container_("a")));
+                set = {"example-schema:a2", "example-schema:leafa"};
+            }
+
+            REQUIRE(ys.childNodes(path) == set);
+        }
+    }
+
+    SECTION("negative")
+    {
+        SECTION("nonexistent nodes")
+        {
+            SECTION("example-schema:coze")
+            {
+                node.first = "example-schema";
+                node.second = "coze";
+            }
+
+            SECTION("example-schema:a/nevim")
+            {
+                path.m_nodes.push_back(node_(module_{"example-schema"}, container_("a")));
+                node.second = "nevim";
+            }
+
+            SECTION("modul:a/nevim")
+            {
+                path.m_nodes.push_back(node_(module_{"modul"}, container_("a")));
+                node.second = "nevim";
+            }
+
+            REQUIRE(!ys.isPresenceContainer(path, node));
+            REQUIRE(!ys.isList(path, node));
+            REQUIRE(!ys.isLeaf(path, node));
+            REQUIRE(!ys.isContainer(path, node));
+        }
+
+        SECTION("\"is\" methods return false for existing nodes for different nodetypes")
+        {
+            SECTION("example-schema:a")
+            {
+                node.first = "example-schema";
+                node.second = "a";
+            }
+
+            SECTION("example-schema:a/a2")
+            {
+                path.m_nodes.push_back(node_(module_{"example-schema"}, container_("a")));
+                node.second = "a2";
+            }
+
+            REQUIRE(!ys.isPresenceContainer(path, node));
+            REQUIRE(!ys.isList(path, node));
+            REQUIRE(!ys.isLeaf(path, node));
+        }
+
+        SECTION("nodetype-specific methods called with different nodetypes")
+        {
+            path.m_nodes.push_back(node_(module_{"example-schema"}, container_("a")));
+            node.second = "a2";
+
+            REQUIRE(!ys.leafEnumHasValue(path, node, "haha"));
+            REQUIRE(!ys.listHasKey(path, node, "chacha"));
+        }
+
+        SECTION("nonexistent module")
+        {
+            REQUIRE(!ys.isModule(path, "notAModule"));
+        }
+    }
+}