Add node-info getters to Schema

Change-Id: Iffea96cdcf20763286db19b0ee2b92dbf7c69b6c
diff --git a/src/schema.hpp b/src/schema.hpp
index a85961b..e4636cf 100644
--- a/src/schema.hpp
+++ b/src/schema.hpp
@@ -76,6 +76,8 @@
     virtual const std::set<std::string> listKeys(const schemaPath_& location, const ModuleNodePair& node) const = 0;
     virtual yang::LeafDataTypes leafType(const schemaPath_& location, const ModuleNodePair& node) const = 0;
     virtual yang::LeafDataTypes leafrefBase(const schemaPath_& location, const ModuleNodePair& node) const = 0;
+    virtual std::optional<std::string> description(const std::string& location) const = 0;
+    virtual std::optional<std::string> units(const std::string& location) const = 0;
 
     virtual const std::set<std::string> validIdentities(const schemaPath_& location, const ModuleNodePair& node, const Prefixes prefixes) const = 0;
     virtual const std::set<std::string> enumValues(const schemaPath_& location, const ModuleNodePair& node) const = 0;
diff --git a/src/static_schema.cpp b/src/static_schema.cpp
index d2287dc..9b4f00b 100644
--- a/src/static_schema.cpp
+++ b/src/static_schema.cpp
@@ -271,6 +271,16 @@
     }
 }
 
+std::optional<std::string> StaticSchema::description([[maybe_unused]] const std::string& path) const
+{
+    throw std::runtime_error{"StaticSchema::description not implemented"};
+}
+
+std::optional<std::string> StaticSchema::units([[maybe_unused]] const std::string& path) const
+{
+    throw std::runtime_error{"StaticSchema::units not implemented"};
+}
+
 yang::NodeTypes StaticSchema::nodeType([[maybe_unused]] const std::string& path) const
 {
     throw std::runtime_error{"Internal error: StaticSchema::nodeType(std::string) not implemented. The tests should not have called this overload."};
diff --git a/src/static_schema.hpp b/src/static_schema.hpp
index c2623a5..a63f92c 100644
--- a/src/static_schema.hpp
+++ b/src/static_schema.hpp
@@ -61,6 +61,8 @@
     const std::set<std::string> validIdentities(const schemaPath_& location, const ModuleNodePair& node, const Prefixes prefixes) const override;
     std::set<std::string> childNodes(const schemaPath_& path, const Recursion) const override;
     std::set<std::string> moduleNodes(const module_& module, const Recursion recursion) const override;
+    std::optional<std::string> description(const std::string& path) const override;
+    std::optional<std::string> units(const std::string& path) const override;
 
     void addContainer(const std::string& location, const std::string& name, yang::ContainerTraits isPresence = yang::ContainerTraits::None);
     void addLeaf(const std::string& location, const std::string& name, const yang::LeafDataTypes& type);
diff --git a/src/yang_schema.cpp b/src/yang_schema.cpp
index 10565d3..d915eef 100644
--- a/src/yang_schema.cpp
+++ b/src/yang_schema.cpp
@@ -416,3 +416,34 @@
 {
     return impl_nodeType(getSchemaNode(path));
 }
+
+std::optional<std::string> YangSchema::description(const std::string& path) const
+{
+    auto node = getSchemaNode(path.c_str());
+    return node->dsc() ? std::optional{node->dsc()} : std::nullopt;
+}
+
+std::optional<std::string> YangSchema::units(const std::string& path) const
+{
+    auto node = getSchemaNode(path.c_str());
+    if (node->nodetype() != LYS_LEAF) {
+        return std::nullopt;
+    }
+    libyang::Schema_Node_Leaf leaf{node};
+    auto units = leaf.units();
+
+    // A leaf can specify units as part of its definition.
+    if (units) {
+        return units;
+    }
+
+    // A typedef (or its parent typedefs) can specify units too. We'll use the first `units` we find.
+    for (auto parentTypedef = leaf.type()->der(); parentTypedef; parentTypedef = parentTypedef->type()->der()) {
+        units = parentTypedef->units();
+        if (units) {
+            return units;
+        }
+    }
+
+    return std::nullopt;
+}
diff --git a/src/yang_schema.hpp b/src/yang_schema.hpp
index ea204e2..8471bea 100644
--- a/src/yang_schema.hpp
+++ b/src/yang_schema.hpp
@@ -43,6 +43,8 @@
     const std::set<std::string> enumValues(const schemaPath_& location, const ModuleNodePair& node) const override;
     std::set<std::string> childNodes(const schemaPath_& path, const Recursion recursion) const override;
     std::set<std::string> moduleNodes(const module_& module, const Recursion recursion) const override;
+    std::optional<std::string> description(const std::string& path) const override;
+    std::optional<std::string> units(const std::string& path) const override;
 
     void registerModuleCallback(const std::function<std::string(const char*, const char*, const char*, const char*)>& clb);
 
diff --git a/tests/datastore_access.cpp b/tests/datastore_access.cpp
index ba52b9c..311b395 100644
--- a/tests/datastore_access.cpp
+++ b/tests/datastore_access.cpp
@@ -27,25 +27,6 @@
     IMPLEMENT_MOCK3(write);
 };
 
-namespace std {
-std::ostream& operator<<(std::ostream& s, const std::optional<std::string>& opt)
-{
-    s << (opt ? *opt : "std::nullopt");
-    return s;
-}
-
-std::ostream& operator<<(std::ostream& s, const DatastoreAccess::Tree& map)
-{
-    s << std::endl
-      << "{";
-    for (const auto& it : map) {
-        s << "{\"" << it.first << "\", " << leafDataToString(it.second) << "}" << std::endl;
-    }
-    s << "}" << std::endl;
-    return s;
-}
-}
-
 TEST_CASE("setting/getting values")
 {
     trompeloeil::sequence seq1;
diff --git a/tests/pretty_printers.hpp b/tests/pretty_printers.hpp
index 42be33e..86bd20d 100644
--- a/tests/pretty_printers.hpp
+++ b/tests/pretty_printers.hpp
@@ -7,6 +7,7 @@
 
 #include <experimental/iterator>
 #include "parser.hpp"
+#include "utils.hpp"
 namespace std {
 std::ostream& operator<<(std::ostream& s, const Completions& completion)
 {
@@ -18,4 +19,21 @@
     s << "}" << std::endl;
     return s;
 }
+
+std::ostream& operator<<(std::ostream& s, const std::optional<std::string>& opt)
+{
+    s << (opt ? *opt : "std::nullopt");
+    return s;
+}
+
+std::ostream& operator<<(std::ostream& s, const DatastoreAccess::Tree& map)
+{
+    s << std::endl
+      << "{";
+    for (const auto& it : map) {
+        s << "{\"" << it.first << "\", " << leafDataToString(it.second) << "}" << std::endl;
+    }
+    s << "}" << std::endl;
+    return s;
+}
 }
diff --git a/tests/yang.cpp b/tests/yang.cpp
index 017107c..5f2f0c3 100644
--- a/tests/yang.cpp
+++ b/tests/yang.cpp
@@ -7,6 +7,7 @@
 */
 
 #include <experimental/iterator>
+#include "pretty_printers.hpp"
 #include "trompeloeil_doctest.hpp"
 #include "yang_schema.hpp"
 
@@ -114,6 +115,7 @@
     }
 
     leaf leafInt32 {
+        description "A 32-bit integer leaf.";
         type int32;
     }
 
@@ -262,6 +264,32 @@
         }
     }
 
+    leaf length {
+        type int32;
+        units "m";
+    }
+
+    leaf wavelength {
+        type decimal64 {
+            fraction-digits 10;
+        }
+        units "nm";
+    }
+
+    typedef seconds {
+        type int32;
+        units "s";
+    }
+
+    leaf duration {
+        type seconds;
+    }
+
+    leaf another-duration {
+        type seconds;
+        units "vt";
+    }
+
 })";
 
 namespace std {
@@ -726,7 +754,9 @@
                        "example-schema:carry", "example-schema:zero", "example-schema:direction",
                        "example-schema:interrupt",
                        "example-schema:ethernet", "example-schema:loopback",
-                       "example-schema:pizzaSize"};
+                       "example-schema:pizzaSize",
+                       "example-schema:length", "example-schema:wavelength",
+                       "example-schema:duration", "example-schema:another-duration"};
             }
 
             SECTION("example-schema:a")
@@ -780,6 +810,90 @@
 
             REQUIRE(ys.nodeType(pathToSchemaString(path, Prefixes::WhenNeeded)) == expected);
         }
+
+        SECTION("description")
+        {
+            std::optional<std::string> expected;
+            SECTION("leafInt32")
+            {
+                path.m_nodes.push_back(schemaNode_(module_{"example-schema"}, leaf_("leafInt32")));
+                expected = "A 32-bit integer leaf.";
+            }
+
+            SECTION("leafString")
+            {
+                path.m_nodes.push_back(schemaNode_(module_{"example-schema"}, leaf_("leafString")));
+            }
+
+            REQUIRE(ys.description(pathToSchemaString(path, Prefixes::WhenNeeded)) == expected);
+        }
+
+        SECTION("units")
+        {
+            std::optional<std::string> expected;
+            SECTION("length")
+            {
+                path.m_nodes.push_back(schemaNode_(module_{"example-schema"}, leaf_("length")));
+                expected = "m";
+            }
+
+            SECTION("wavelength")
+            {
+                path.m_nodes.push_back(schemaNode_(module_{"example-schema"}, leaf_("wavelength")));
+                expected = "nm";
+            }
+
+            SECTION("leafInt32")
+            {
+                path.m_nodes.push_back(schemaNode_(module_{"example-schema"}, leaf_("leafInt32")));
+            }
+
+            SECTION("duration")
+            {
+                path.m_nodes.push_back(schemaNode_(module_{"example-schema"}, leaf_("duration")));
+                expected = "s";
+            }
+
+            SECTION("another-duration")
+            {
+                path.m_nodes.push_back(schemaNode_(module_{"example-schema"}, leaf_("another-duration")));
+                expected = "vt";
+            }
+
+            REQUIRE(ys.units(pathToSchemaString(path, Prefixes::WhenNeeded)) == expected);
+        }
+
+        SECTION("nodeType")
+        {
+            yang::NodeTypes expected;
+            SECTION("leafInt32")
+            {
+                path.m_nodes.push_back(schemaNode_(module_{"example-schema"}, leaf_("leafInt32")));
+                expected = yang::NodeTypes::Leaf;
+            }
+
+            SECTION("a")
+            {
+                path.m_nodes.push_back(schemaNode_(module_{"example-schema"}, container_("a")));
+                expected = yang::NodeTypes::Container;
+            }
+
+            SECTION("a/a2/a3")
+            {
+                path.m_nodes.push_back(schemaNode_(module_{"example-schema"}, container_("a")));
+                path.m_nodes.push_back(schemaNode_(container_("a2")));
+                path.m_nodes.push_back(schemaNode_(container_("a3")));
+                expected = yang::NodeTypes::PresenceContainer;
+            }
+
+            SECTION("_list")
+            {
+                path.m_nodes.push_back(schemaNode_(module_{"example-schema"}, list_("_list")));
+                expected = yang::NodeTypes::List;
+            }
+
+            REQUIRE(ys.nodeType(pathToSchemaString(path, Prefixes::WhenNeeded)) == expected);
+        }
     }
 
     SECTION("negative")