utils: refactor utils::getSubtree libyang wrapper

In the change for the commit to follow Vasek suggested that
the utils::getSubtree helper could be renamed to getUniqueSubtree and
the behaviour could be changed to return std::optional<>. When no
matching node is found the function returns std::nullopt. When multiple
matching nodes are found the function throws.

This code in the next commit could benefit a little from this change.

Change-Id: I8ded7695dc383e6654f8efa44b6a5442ef904485
diff --git a/src/system/Authentication.cpp b/src/system/Authentication.cpp
index f4d10ed..35c6c7d 100644
--- a/src/system/Authentication.cpp
+++ b/src/system/Authentication.cpp
@@ -301,9 +301,9 @@
             auto,
             auto output) {
 
-        auto userNode = utils::getSubtree(input, (authentication_container + "/users" ).c_str());
-        auto name = utils::getValueAsString(utils::getSubtree(userNode, "name"));
-        auto password = utils::getValueAsString(utils::getSubtree(userNode, "change-password/password-cleartext"));
+        auto userNode = utils::getUniqueSubtree(input, (authentication_container + "/users" ).c_str()).value();
+        auto name = utils::getValueAsString(utils::getUniqueSubtree(userNode, "name").value());
+        auto password = utils::getValueAsString(utils::getUniqueSubtree(userNode, "change-password/password-cleartext").value());
         m_log->debug("Changing password for {}", name);
         try {
             changePassword(name, password, m_etc_shadow);
@@ -326,9 +326,9 @@
             auto,
             auto output) {
 
-        auto userNode = utils::getSubtree(input, (authentication_container + "/users").c_str());
-        auto name = utils::getValueAsString(utils::getSubtree(userNode, "name"));
-        auto key = utils::getValueAsString(utils::getSubtree(userNode, "add-authorized-key/key"));
+        auto userNode = utils::getUniqueSubtree(input, (authentication_container + "/users").c_str()).value();
+        auto name = utils::getValueAsString(utils::getUniqueSubtree(userNode, "name").value());
+        auto key = utils::getValueAsString(utils::getUniqueSubtree(userNode, "add-authorized-key/key").value());
         m_log->debug("Adding key for {}", name);
         try {
             addKey(name, key);
@@ -351,9 +351,9 @@
             auto,
             auto output) {
 
-        auto userNode = utils::getSubtree(input, (authentication_container + "/users").c_str());
-        auto name = utils::getValueAsString(utils::getSubtree(userNode, "name"));
-        auto key = std::stol(utils::getValueAsString(utils::getSubtree(userNode, "authorized-keys/index")));
+        auto userNode = utils::getUniqueSubtree(input, (authentication_container + "/users").c_str()).value();
+        auto name = utils::getValueAsString(utils::getUniqueSubtree(userNode, "name").value());
+        auto key = std::stol(utils::getValueAsString(utils::getUniqueSubtree(userNode, "authorized-keys/index").value()));
         m_log->debug("Removing key for {}", name);
         try {
             removeKey(name, key);
diff --git a/src/system/IETFInterfacesConfig.cpp b/src/system/IETFInterfacesConfig.cpp
index f28f124..9ac681a 100644
--- a/src/system/IETFInterfacesConfig.cpp
+++ b/src/system/IETFInterfacesConfig.cpp
@@ -51,12 +51,11 @@
 {
     const auto xpath = "ietf-ip:" + proto + "/enabled";
 
-    try {
-        auto enabled = velia::utils::getValueAsString(velia::utils::getSubtree(linkEntry, xpath.c_str()));
-        return enabled == "true"s;
-    } catch (const std::runtime_error&) { // leaf and the presence container missing
-        return false;
+    if (auto node = velia::utils::getUniqueSubtree(linkEntry, xpath.c_str())) {
+        return velia::utils::getValueAsString(node.value()) == "true"s;
     }
+
+    return false;
 }
 }
 
@@ -90,7 +89,7 @@
         for (const auto& linkEntry : linkEntries->data()) {
             std::map<std::string, std::vector<std::string>> configValues;
 
-            auto linkName = utils::getValueAsString(utils::getSubtree(linkEntry, "name"));
+            auto linkName = utils::getValueAsString(utils::getUniqueSubtree(linkEntry, "name").value());
 
             if (auto set = linkEntry->find_path("description"); set->number() != 0) {
                 configValues["Network"].push_back("Description="s + utils::getValueAsString(set->data().front()));
@@ -107,8 +106,8 @@
                 const auto addresses = linkEntry->find_path(IPAddressListXPath.c_str());
 
                 for (const auto& ipEntry : addresses->data()) {
-                    auto ipAddress = utils::getValueAsString(utils::getSubtree(ipEntry, "ip"));
-                    auto prefixLen = utils::getValueAsString(utils::getSubtree(ipEntry, "prefix-length"));
+                    auto ipAddress = utils::getValueAsString(utils::getUniqueSubtree(ipEntry, "ip").value());
+                    auto prefixLen = utils::getValueAsString(utils::getUniqueSubtree(ipEntry, "prefix-length").value());
 
                     spdlog::get("system")->trace("Link {}: address {}/{} configured", linkName, ipAddress, prefixLen);
                     configValues["Network"].push_back("Address="s + ipAddress + "/" + prefixLen);
diff --git a/src/system/LED.cpp b/src/system/LED.cpp
index 1d0b68a..f658644 100644
--- a/src/system/LED.cpp
+++ b/src/system/LED.cpp
@@ -54,7 +54,7 @@
     m_srSubscribe->rpc_subscribe_tree(
         (CZECHLIGHT_SYSTEM_LEDS_MODULE_PREFIX + "uid").c_str(),
         [this, uidMaxBrightness, triggerFile, brightnessFile](auto session, auto, auto input, auto, auto, auto) {
-            std::string val = utils::getValueAsString(utils::getSubtree(input, (CZECHLIGHT_SYSTEM_LEDS_MODULE_PREFIX + "uid/state").c_str()));
+            std::string val = utils::getValueAsString(utils::getUniqueSubtree(input, (CZECHLIGHT_SYSTEM_LEDS_MODULE_PREFIX + "uid/state").c_str()).value());
 
             try {
                 if (val == "on") {
diff --git a/src/utils/libyang.cpp b/src/utils/libyang.cpp
index d018a09..a4b7479 100644
--- a/src/utils/libyang.cpp
+++ b/src/utils/libyang.cpp
@@ -13,13 +13,17 @@
     return libyang::Data_Node_Leaf_List(node).value_str();
 }
 
-libyang::S_Data_Node getSubtree(const libyang::S_Data_Node& start, const char* path)
+std::optional<libyang::S_Data_Node> getUniqueSubtree(const libyang::S_Data_Node& start, const char* path)
 {
     auto set = start->find_path(path);
-    if (set->number() != 1) {
-        throw std::runtime_error(fmt::format("getSubtree({}, {}): didn't get exactly one match (got {})", start->path(), path, set->number()));
-    }
 
-    return set->data().front();
+    switch(set->number()) {
+    case 0:
+        return std::nullopt;
+    case 1:
+        return set->data().front();
+    default:
+        throw std::runtime_error(fmt::format("getUniqueSubtree({}, {}): more than one match (got {})", start->path(), path, set->number()));
+    }
 }
 }
diff --git a/src/utils/libyang.h b/src/utils/libyang.h
index 4ccc59d..389c1ac 100644
--- a/src/utils/libyang.h
+++ b/src/utils/libyang.h
@@ -7,6 +7,7 @@
 
 #pragma once
 #include <memory>
+#include <optional>
 
 namespace libyang {
     class Data_Node;
@@ -24,7 +25,7 @@
 
 /** @brief Gets exactly one node based on `path` starting from `start`.
  *
- * Throws if there is more than one matching node. Also throws if there aren't any matching nodes.
+ * Throws if there is more than one matching node. Returns std::nullopt if no node matches.
  */
-std::shared_ptr<libyang::Data_Node> getSubtree(const std::shared_ptr<libyang::Data_Node>& start, const char* path);
+std::optional<std::shared_ptr<libyang::Data_Node>> getUniqueSubtree(const std::shared_ptr<libyang::Data_Node>& start, const char* path);
 }