Merge support for the `desc` command

Change-Id: I787cd1287b15e760f50f2f40137262b3d5f1a02d
diff --git a/src/ast_commands.hpp b/src/ast_commands.hpp
index 7f3160a..397aeb8 100644
--- a/src/ast_commands.hpp
+++ b/src/ast_commands.hpp
@@ -144,8 +144,27 @@
     boost::optional<boost::variant<boost::variant<dataPath_, schemaPath_>, module_>> m_path;
 };
 
+struct describe_ : x3::position_tagged {
+    static constexpr auto name = "describe";
+    static constexpr auto shortHelp = "describe - Print information about YANG tree path.";
+    static constexpr auto longHelp = R"(
+    describe <path>
+
+    Show documentation of YANG tree paths. In the YANG model, each item may
+    have an optional `description` which often explains the function of that
+    node to the end user. This command takes the description from the YANG
+    model and shows it to the user along with additional data, such as the type
+    of the node, units of leaf values, etc.
+
+    Usage:
+        /> describe /module:node)";
+    bool operator==(const describe_& b) const;
+
+    boost::variant<schemaPath_, dataPath_> m_path;
+};
+
 struct help_;
-using CommandTypes = boost::mpl::vector<discard_, ls_, cd_, create_, delete_, set_, commit_, get_, help_>;
+using CommandTypes = boost::mpl::vector<discard_, ls_, cd_, create_, delete_, set_, commit_, get_, describe_, help_>;
 struct help_ : x3::position_tagged {
     static constexpr auto name = "help";
     static constexpr auto shortHelp = "help - Print help for commands.";
@@ -186,6 +205,7 @@
 BOOST_FUSION_ADAPT_STRUCT(binary_, m_value)
 BOOST_FUSION_ADAPT_STRUCT(identityRef_, m_prefix, m_value)
 BOOST_FUSION_ADAPT_STRUCT(commit_)
+BOOST_FUSION_ADAPT_STRUCT(describe_, m_path)
 BOOST_FUSION_ADAPT_STRUCT(help_, m_cmd)
 BOOST_FUSION_ADAPT_STRUCT(discard_)
 BOOST_FUSION_ADAPT_STRUCT(get_, m_path)
diff --git a/src/ast_handlers.hpp b/src/ast_handlers.hpp
index 04e890a..a8dd4ae 100644
--- a/src/ast_handlers.hpp
+++ b/src/ast_handlers.hpp
@@ -426,7 +426,7 @@
 
         auto type = schema.leafType(parserContext.m_tmpListKeyLeafPath.m_location, parserContext.m_tmpListKeyLeafPath.m_node);
         if (type == yang::LeafDataTypes::LeafRef) {
-            type = schema.leafrefBase(parserContext.m_tmpListKeyLeafPath.m_location, parserContext.m_tmpListKeyLeafPath.m_node);
+            type = schema.leafrefBaseType(parserContext.m_tmpListKeyLeafPath.m_location, parserContext.m_tmpListKeyLeafPath.m_node);
         }
 
         if (type != TYPE) {
@@ -497,6 +497,8 @@
 
 struct commit_class;
 
+struct describe_class;
+
 struct help_class;
 
 struct get_class;
diff --git a/src/grammars.hpp b/src/grammars.hpp
index dd13c6a..3159fe0 100644
--- a/src/grammars.hpp
+++ b/src/grammars.hpp
@@ -65,6 +65,7 @@
 x3::rule<create_class, create_> const create = "create";
 x3::rule<delete_class, delete_> const delete_rule = "delete_rule";
 x3::rule<commit_class, commit_> const commit = "commit";
+x3::rule<describe_class, describe_> const describe = "describe";
 x3::rule<help_class, help_> const help = "help";
 x3::rule<command_class, command_> const command = "command";
 
@@ -332,11 +333,14 @@
 auto const help_def =
     help_::name > createCommandSuggestions >> -command_names;
 
+auto const describe_def =
+    describe_::name >> space_separator > (dataPathListEnd | dataPath | schemaPath);
+
 auto const createCommandSuggestions_def =
     x3::eps;
 
 auto const command_def =
-    createCommandSuggestions >> x3::expect[cd | create | delete_rule | set | commit | get | ls | discard | help];
+    createCommandSuggestions >> x3::expect[cd | create | delete_rule | set | commit | get | ls | discard | describe | help];
 
 #if __clang__
 #pragma GCC diagnostic pop
@@ -393,6 +397,7 @@
 BOOST_SPIRIT_DEFINE(cd)
 BOOST_SPIRIT_DEFINE(create)
 BOOST_SPIRIT_DEFINE(delete_rule)
+BOOST_SPIRIT_DEFINE(describe)
 BOOST_SPIRIT_DEFINE(help)
 BOOST_SPIRIT_DEFINE(command)
 BOOST_SPIRIT_DEFINE(createPathSuggestions)
diff --git a/src/interpreter.cpp b/src/interpreter.cpp
index f112b52..5b13337 100644
--- a/src/interpreter.cpp
+++ b/src/interpreter.cpp
@@ -8,6 +8,7 @@
 
 #include <boost/mpl/for_each.hpp>
 #include <iostream>
+#include <sstream>
 #include "datastore_access.hpp"
 #include "interpreter.hpp"
 #include "utils.hpp"
@@ -79,6 +80,60 @@
         std::cout << it << std::endl;
 }
 
+std::string Interpreter::buildTypeInfo(const std::string& path) const
+{
+    std::ostringstream ss;
+    switch (m_datastore.schema()->nodeType(path)) {
+    case yang::NodeTypes::Container:
+        ss << "container";
+        break;
+    case yang::NodeTypes::PresenceContainer:
+        ss << "presence container";
+        break;
+    case yang::NodeTypes::Leaf:
+    {
+        auto leafType = m_datastore.schema()->leafType(path);
+        auto typedefName = m_datastore.schema()->leafTypeName(path);
+        std::string baseTypeStr;
+        if (leafType == yang::LeafDataTypes::LeafRef) {
+            ss << "-> ";
+            ss << m_datastore.schema()->leafrefPath(path) << " ";
+            baseTypeStr = leafDataTypeToString(m_datastore.schema()->leafrefBaseType(path));
+        } else {
+            baseTypeStr = leafDataTypeToString(leafType);
+        }
+
+        if (typedefName) {
+            ss << *typedefName << " (" << baseTypeStr << ")";
+        } else {
+            ss << baseTypeStr;
+        }
+
+        if (auto units = m_datastore.schema()->units(path)) {
+            ss << " [" + *units + "]";
+        }
+
+        if (m_datastore.schema()->leafIsKey(path)) {
+            ss << " (key)";
+        }
+        break;
+    }
+    case yang::NodeTypes::List:
+        ss << "list";
+        break;
+    }
+    return ss.str();
+}
+
+void Interpreter::operator()(const describe_& describe) const
+{
+    auto path = absolutePathFromCommand(describe);
+    std::cout << path << ": " << buildTypeInfo(path) << std::endl;
+    if (auto description = m_datastore.schema()->description(path)) {
+        std::cout << std::endl << *description << std::endl;
+    }
+}
+
 struct commandLongHelpVisitor : boost::static_visitor<const char*> {
     template <typename T>
     auto constexpr operator()(boost::type<T>) const
@@ -156,6 +211,15 @@
     }
 }
 
+std::string Interpreter::absolutePathFromCommand(const describe_& describe) const
+{
+    auto pathStr = boost::apply_visitor(pathToStringVisitor(), describe.m_path);
+    if (boost::apply_visitor(getPathScopeVisitor(), describe.m_path) == Scope::Absolute)
+        return pathStr;
+    else
+        return joinPaths(m_parser.currentNode(), pathStr);
+}
+
 Interpreter::Interpreter(Parser& parser, DatastoreAccess& datastore)
     : m_parser(parser)
     , m_datastore(datastore)
diff --git a/src/interpreter.hpp b/src/interpreter.hpp
index 7af5983..a05183f 100644
--- a/src/interpreter.hpp
+++ b/src/interpreter.hpp
@@ -22,6 +22,7 @@
     void operator()(const create_&) const;
     void operator()(const delete_&) const;
     void operator()(const ls_&) const;
+    void operator()(const describe_&) const;
     void operator()(const discard_&) const;
     void operator()(const help_&) const;
 
@@ -29,6 +30,8 @@
     template <typename T>
     std::string absolutePathFromCommand(const T& command) const;
     std::string absolutePathFromCommand(const get_& command) const;
+    std::string absolutePathFromCommand(const describe_& describe) const;
+    std::string buildTypeInfo(const std::string& path) const;
 
     Parser& m_parser;
     DatastoreAccess& m_datastore;
diff --git a/src/schema.hpp b/src/schema.hpp
index a85961b..ea0deba 100644
--- a/src/schema.hpp
+++ b/src/schema.hpp
@@ -73,9 +73,16 @@
     virtual bool leafEnumHasValue(const schemaPath_& location, const ModuleNodePair& node, const std::string& value) const = 0;
     virtual bool leafIdentityIsValid(const schemaPath_& location, const ModuleNodePair& node, const ModuleValuePair& value) const = 0;
     virtual bool listHasKey(const schemaPath_& location, const ModuleNodePair& node, const std::string& key) const = 0;
+    virtual bool leafIsKey(const std::string& leafPath) const = 0;
     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 yang::LeafDataTypes leafType(const std::string& path) const = 0;
+    virtual std::optional<std::string> leafTypeName(const std::string& path) const = 0;
+    virtual yang::LeafDataTypes leafrefBaseType(const schemaPath_& location, const ModuleNodePair& node) const = 0;
+    virtual yang::LeafDataTypes leafrefBaseType(const std::string& path) const = 0;
+    virtual std::string leafrefPath(const std::string& leafrefPath) 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..87d190a 100644
--- a/src/static_schema.cpp
+++ b/src/static_schema.cpp
@@ -189,7 +189,7 @@
     return res;
 }
 
-yang::LeafDataTypes StaticSchema::leafrefBase(const schemaPath_& location, const ModuleNodePair& node) const
+yang::LeafDataTypes StaticSchema::leafrefBaseType(const schemaPath_& location, const ModuleNodePair& node) const
 {
     std::string locationString = pathToSchemaString(location, Prefixes::Always);
     auto leaf{boost::get<yang::leaf>(children(locationString).at(fullNodeName(location, node)))};
@@ -204,6 +204,11 @@
     return boost::get<yang::leaf>(children(locationString).at(fullNodeName(location, node))).m_type;
 }
 
+yang::LeafDataTypes StaticSchema::leafType([[maybe_unused]] const std::string& path) const
+{
+    throw std::runtime_error{"StaticSchema::leafType not implemented"};
+}
+
 const std::set<std::string> StaticSchema::enumValues(const schemaPath_& location, const ModuleNodePair& node) const
 {
     std::string locationString = pathToSchemaString(location, Prefixes::Always);
@@ -271,7 +276,37 @@
     }
 }
 
+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."};
 }
+
+yang::LeafDataTypes StaticSchema::leafrefBaseType([[maybe_unused]] const std::string& path) const
+{
+    throw std::runtime_error{"Internal error: StaticSchema::leafrefBaseType(std::string) not implemented. The tests should not have called this overload."};
+}
+
+std::string StaticSchema::leafrefPath([[maybe_unused]] const std::string& leafrefPath) const
+{
+    throw std::runtime_error{"Internal error: StaticSchema::leafrefPath(std::string) not implemented. The tests should not have called this overload."};
+}
+
+bool StaticSchema::leafIsKey([[maybe_unused]] const std::string& leafPath) const
+{
+    throw std::runtime_error{"Internal error: StaticSchema::leafIsKey(std::string) not implemented. The tests should not have called this overload."};
+}
+
+std::optional<std::string> StaticSchema::leafTypeName([[maybe_unused]] const std::string& path) const
+{
+    throw std::runtime_error{"Internal error: StaticSchema::leafTypeName(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..97f1e7e 100644
--- a/src/static_schema.hpp
+++ b/src/static_schema.hpp
@@ -54,13 +54,20 @@
     bool leafEnumHasValue(const schemaPath_& location, const ModuleNodePair& node, const std::string& value) const override;
     bool leafIdentityIsValid(const schemaPath_& location, const ModuleNodePair& node, const ModuleValuePair& value) const override;
     bool listHasKey(const schemaPath_& location, const ModuleNodePair& node, const std::string& key) const override;
+    bool leafIsKey(const std::string& leafPath) const override;
     const std::set<std::string> listKeys(const schemaPath_& location, const ModuleNodePair& node) const override;
     yang::LeafDataTypes leafType(const schemaPath_& location, const ModuleNodePair& node) const override;
-    yang::LeafDataTypes leafrefBase(const schemaPath_& location, const ModuleNodePair& node) const override;
+    yang::LeafDataTypes leafType(const std::string& path) const override;
+    std::optional<std::string> leafTypeName(const std::string& path) const override;
+    yang::LeafDataTypes leafrefBaseType(const schemaPath_& location, const ModuleNodePair& node) const override;
+    yang::LeafDataTypes leafrefBaseType(const std::string& path) const override;
+    std::string leafrefPath(const std::string& leafrefPath) const override;
     const std::set<std::string> enumValues(const schemaPath_& location, const ModuleNodePair& node) const override;
     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 88f0154..e18c780 100644
--- a/src/yang_schema.cpp
+++ b/src/yang_schema.cpp
@@ -175,6 +175,15 @@
     return keys.find(key) != keys.end();
 }
 
+bool YangSchema::leafIsKey(const std::string& leafPath) const
+{
+    auto node = getSchemaNode(leafPath);
+    if (!node || node->nodetype() != LYS_LEAF)
+        return false;
+
+    return libyang::Schema_Node_Leaf{node}.is_key().get();
+}
+
 libyang::S_Schema_Node YangSchema::impl_getSchemaNode(const std::string& node) const
 {
     // If no node is found find_path prints an error message, so we have to
@@ -257,31 +266,65 @@
     }
 }
 
-yang::LeafDataTypes YangSchema::leafType(const schemaPath_& location, const ModuleNodePair& node) const
+namespace {
+yang::LeafDataTypes impl_leafType(const libyang::S_Schema_Node& node)
 {
     using namespace std::string_literals;
-    if (!isLeaf(location, node))
-        throw InvalidSchemaQueryException(fullNodeName(location, node) + " is not a leaf");
-
-    libyang::Schema_Node_Leaf leaf(getSchemaNode(location, node));
+    libyang::Schema_Node_Leaf leaf(node);
     auto baseType{leaf.type()->base()};
     try {
         return lyTypeToLeafDataTypes(baseType);
     } catch (std::logic_error& ex) {
-        throw UnsupportedYangTypeException("the type of "s + fullNodeName(location, node) + " is not supported: " + ex.what());
+        throw UnsupportedYangTypeException("the type of "s + node->name() + " is not supported: " + ex.what());
     }
 }
+}
 
-yang::LeafDataTypes YangSchema::leafrefBase(const schemaPath_& location, const ModuleNodePair& node) const
+yang::LeafDataTypes YangSchema::leafType(const schemaPath_& location, const ModuleNodePair& node) const
+{
+    return impl_leafType(getSchemaNode(location, node));
+}
+
+yang::LeafDataTypes YangSchema::leafType(const std::string& path) const
+{
+    return impl_leafType(getSchemaNode(path));
+}
+
+std::optional<std::string> YangSchema::leafTypeName(const std::string& path) const
+{
+    libyang::Schema_Node_Leaf leaf(getSchemaNode(path));
+    return leaf.type()->der().get() ? std::optional{leaf.type()->der()->name()} : std::nullopt;
+}
+
+namespace {
+yang::LeafDataTypes impl_leafrefBaseType(const libyang::S_Schema_Node& node)
 {
     using namespace std::string_literals;
-    libyang::Schema_Node_Leaf leaf(getSchemaNode(location, node));
+    libyang::Schema_Node_Leaf leaf(node);
     try {
         return lyTypeToLeafDataTypes(leaf.type()->info()->lref()->target()->type()->base());
     } catch (std::logic_error& ex) {
-        throw UnsupportedYangTypeException("the type of "s + fullNodeName(location, node) + " is not supported: " + ex.what());
+        throw UnsupportedYangTypeException("the type of "s + node->name() + " is not supported: " + ex.what());
     }
 }
+}
+
+yang::LeafDataTypes YangSchema::leafrefBaseType(const schemaPath_& location, const ModuleNodePair& node) const
+{
+    return impl_leafrefBaseType(getSchemaNode(location, node));
+}
+
+yang::LeafDataTypes YangSchema::leafrefBaseType(const std::string& path) const
+{
+    return impl_leafrefBaseType(getSchemaNode(path));
+}
+
+std::string YangSchema::leafrefPath(const std::string& leafrefPath) const
+{
+    using namespace std::string_literals;
+    libyang::Schema_Node_Leaf leaf(getSchemaNode(leafrefPath));
+    return leaf.type()->info()->lref()->target()->path(LYS_PATH_FIRST_PREFIX);
+}
 
 std::set<std::string> YangSchema::modules() const
 {
@@ -421,3 +464,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..c763ea6 100644
--- a/src/yang_schema.hpp
+++ b/src/yang_schema.hpp
@@ -36,13 +36,20 @@
     bool leafEnumHasValue(const schemaPath_& location, const ModuleNodePair& node, const std::string& value) const override;
     bool leafIdentityIsValid(const schemaPath_& location, const ModuleNodePair& node, const ModuleValuePair& value) const override;
     bool listHasKey(const schemaPath_& location, const ModuleNodePair& node, const std::string& key) const override;
+    bool leafIsKey(const std::string& leafPath) const override;
     const std::set<std::string> listKeys(const schemaPath_& location, const ModuleNodePair& node) const override;
     yang::LeafDataTypes leafType(const schemaPath_& location, const ModuleNodePair& node) const override;
-    yang::LeafDataTypes leafrefBase(const schemaPath_& location, const ModuleNodePair& node) const override;
+    yang::LeafDataTypes leafType(const std::string& path) const override;
+    std::optional<std::string> leafTypeName(const std::string& path) const override;
+    yang::LeafDataTypes leafrefBaseType(const schemaPath_& location, const ModuleNodePair& node) const override;
+    yang::LeafDataTypes leafrefBaseType(const std::string& path) const override;
+    std::string leafrefPath(const std::string& leafrefPath) const override;
     const std::set<std::string> validIdentities(const schemaPath_& location, const ModuleNodePair& node, const Prefixes prefixes) const override;
     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/command_completion.cpp b/tests/command_completion.cpp
index 1d44646..eb4949a 100644
--- a/tests/command_completion.cpp
+++ b/tests/command_completion.cpp
@@ -22,14 +22,14 @@
     SECTION("")
     {
         input = "";
-        expectedCompletions = {"cd", "create", "delete", "set", "commit", "get", "ls", "discard", "help"};
+        expectedCompletions = {"cd", "create", "delete", "set", "commit", "get", "ls", "discard", "help", "describe"};
         expectedContextLength = 0;
     }
 
     SECTION(" ")
     {
         input = " ";
-        expectedCompletions = {"cd", "create", "delete", "set", "commit", "get", "ls", "discard", "help"};
+        expectedCompletions = {"cd", "create", "delete", "set", "commit", "get", "ls", "discard", "help", "describe"};
         expectedContextLength = 0;
     }
 
@@ -43,7 +43,7 @@
     SECTION("d")
     {
         input = "d";
-        expectedCompletions = {"delete", "discard"};
+        expectedCompletions = {"delete", "discard", "describe"};
         expectedContextLength = 1;
     }
 
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/example-schema.yang b/tests/example-schema.yang
index cbd5d8d..f876779 100644
--- a/tests/example-schema.yang
+++ b/tests/example-schema.yang
@@ -57,6 +57,7 @@
     }
 
     leaf leafDecimal {
+        units "nm";
         type decimal64 {
             fraction-digits 9;
         }
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 9af8b47..d189f50 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,38 @@
         }
     }
 
+    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";
+    }
+
+    leaf activeNumber {
+        type leafref {
+            path "/_list/number";
+        }
+    }
+
     rpc myRpc {}
 
 })";
@@ -728,7 +762,10 @@
                        "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",
+                       "example-schema:activeNumber"};
             }
 
             SECTION("example-schema:a")
@@ -782,6 +819,95 @@
 
             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("leafrefPath")
+        {
+            REQUIRE(ys.leafrefPath("/example-schema:activeNumber") == "/example-schema:_list/number");
+        }
     }
 
     SECTION("negative")