Add support for `empty` YANG leaf type

Change-Id: I87eafae9df9accdaa4579ace769996e70da6cb1c
diff --git a/src/ast_values.cpp b/src/ast_values.cpp
index ae5aa6c..a381c1a 100644
--- a/src/ast_values.cpp
+++ b/src/ast_values.cpp
@@ -33,6 +33,8 @@
 {
 }
 
+empty_::empty_() = default;
+
 bool module_::operator<(const module_& b) const
 {
     return this->m_name < b.m_name;
@@ -58,6 +60,16 @@
     return this->m_value < b.m_value;
 }
 
+bool empty_::operator==(const empty_) const
+{
+    return true;
+}
+
+bool empty_::operator<(const empty_) const
+{
+    return false;
+}
+
 bool enum_::operator==(const enum_& b) const
 {
     return this->m_value == b.m_value;
diff --git a/src/ast_values.hpp b/src/ast_values.hpp
index f2d1c7e..9ec8389 100644
--- a/src/ast_values.hpp
+++ b/src/ast_values.hpp
@@ -26,6 +26,12 @@
     std::string m_value;
 };
 
+struct empty_ {
+    empty_();
+    bool operator==(const empty_) const;
+    bool operator<(const empty_) const;
+};
+
 struct module_ {
     bool operator==(const module_& b) const;
     bool operator<(const module_& b) const;
@@ -63,6 +69,7 @@
 
 using leaf_data_ = boost::variant<enum_,
                                   binary_,
+                                  empty_,
                                   identityRef_,
                                   special_,
                                   double,
diff --git a/src/leaf_data.hpp b/src/leaf_data.hpp
index 2f84a05..4330d5a 100644
--- a/src/leaf_data.hpp
+++ b/src/leaf_data.hpp
@@ -117,6 +117,10 @@
     {
         return leaf_data_string.parse(first, last, ctx, rctx, attr);
     }
+    bool operator()(const yang::Empty) const
+    {
+        return x3::attr(empty_{}).parse(first, last, ctx, rctx, attr);
+    }
     template <typename Type>
     void createSetSuggestions(const Type& type) const
     {
diff --git a/src/leaf_data_type.cpp b/src/leaf_data_type.cpp
index 29fb355..b7a3876 100644
--- a/src/leaf_data_type.cpp
+++ b/src/leaf_data_type.cpp
@@ -100,4 +100,8 @@
 {
     return true;
 }
+bool Empty::operator==(const Empty&) const
+{
+    return true;
+}
 }
diff --git a/src/leaf_data_type.hpp b/src/leaf_data_type.hpp
index 470c435..288f765 100644
--- a/src/leaf_data_type.hpp
+++ b/src/leaf_data_type.hpp
@@ -52,6 +52,9 @@
 struct Binary {
     bool operator==(const Binary&) const;
 };
+struct Empty {
+    bool operator==(const Empty&) const;
+};
 struct Enum {
     Enum(std::set<enum_>&& values);
     bool operator==(const Enum& other) const;
@@ -78,6 +81,7 @@
     yang::Uint64,
     yang::Enum,
     yang::Binary,
+    yang::Empty,
     yang::IdentityRef,
     yang::LeafRef,
     yang::Union
diff --git a/src/libyang_utils.cpp b/src/libyang_utils.cpp
index ddb3630..2669151 100644
--- a/src/libyang_utils.cpp
+++ b/src/libyang_utils.cpp
@@ -31,6 +31,8 @@
         return identityRef_{value->ident()->module()->name(), value->ident()->name()};
     case LY_TYPE_BINARY:
         return binary_{value->binary()};
+    case LY_TYPE_EMPTY:
+        return empty_{};
     case LY_TYPE_DEC64:
     {
         auto v = value->dec64();
diff --git a/src/netconf_access.cpp b/src/netconf_access.cpp
index 5fca200..1340332 100644
--- a/src/netconf_access.cpp
+++ b/src/netconf_access.cpp
@@ -82,7 +82,8 @@
 
 void NetconfAccess::setLeaf(const std::string& path, leaf_data_ value)
 {
-    auto node = m_schema->dataNodeFromPath(path, leafDataToString(value));
+    auto lyValue = value.type() == typeid(empty_) ? std::nullopt : std::optional(leafDataToString(value));
+    auto node = m_schema->dataNodeFromPath(path, lyValue);
     doEditFromDataNode(node);
 }
 
diff --git a/src/sysrepo_access.cpp b/src/sysrepo_access.cpp
index b40b9f1..5b01d3e 100644
--- a/src/sysrepo_access.cpp
+++ b/src/sysrepo_access.cpp
@@ -47,6 +47,8 @@
     }
     case SR_BINARY_T:
         return binary_{value->data()->get_binary()};
+    case SR_LEAF_EMPTY_T:
+        return empty_{};
     case SR_DECIMAL64_T:
         return value->data()->get_decimal64();
     case SR_CONTAINER_T:
@@ -71,6 +73,11 @@
         return std::make_shared<sysrepo::Val>(value.m_value.c_str(), SR_BINARY_T);
     }
 
+    sysrepo::S_Val operator()(const empty_) const
+    {
+        return std::make_shared<sysrepo::Val>(nullptr, SR_LEAF_EMPTY_T);
+    }
+
     sysrepo::S_Val operator()(const identityRef_& value) const
     {
         auto res = value.m_prefix ? (value.m_prefix.value().m_name + ":" + value.m_value) : value.m_value;
@@ -113,6 +120,11 @@
         v->set(xpath.c_str(), value.m_value.c_str(), SR_BINARY_T);
     }
 
+    void operator()(const empty_) const
+    {
+        v->set(xpath.c_str(), nullptr, SR_LEAF_EMPTY_T);
+    }
+
     void operator()(const identityRef_& value) const
     {
         v->set(xpath.c_str(), (value.m_prefix.value().m_name + ":" + value.m_value).c_str(), SR_IDENTITYREF_T);
diff --git a/src/utils.cpp b/src/utils.cpp
index 665aedd..3543787 100644
--- a/src/utils.cpp
+++ b/src/utils.cpp
@@ -123,6 +123,10 @@
     {
         return "a leafref";
     }
+    std::string operator()(const yang::Empty&)
+    {
+        return "an empty leaf";
+    }
     std::string operator()(const yang::Union& type)
     {
         std::ostringstream ss;
@@ -163,6 +167,11 @@
         return data.m_value;
     }
 
+    std::string operator()(const empty_) const
+    {
+        return "[empty]";
+    }
+
     std::string operator()(const identityRef_& data) const
     {
         return data.m_prefix ? (data.m_prefix.value().m_name + ":" + data.m_value) : data.m_value;
diff --git a/src/yang_schema.cpp b/src/yang_schema.cpp
index 96fc744..6021cc5 100644
--- a/src/yang_schema.cpp
+++ b/src/yang_schema.cpp
@@ -246,6 +246,9 @@
         case LY_TYPE_BINARY:
             resType.emplace<yang::Binary>();
             break;
+        case LY_TYPE_EMPTY:
+            resType.emplace<yang::Empty>();
+            break;
         case LY_TYPE_ENUM:
             resType.emplace<yang::Enum>(enumValues(type));
             break;
diff --git a/tests/datastore_access.cpp b/tests/datastore_access.cpp
index 51bcd81..088fc4b 100644
--- a/tests/datastore_access.cpp
+++ b/tests/datastore_access.cpp
@@ -359,6 +359,17 @@
         REQUIRE(datastore.getItems("/example-schema:blob") == expected);
     }
 
+    SECTION("empty")
+    {
+        datastore.setLeaf("/example-schema:dummy", empty_{});
+        REQUIRE_CALL(mock, write("/example-schema:dummy", std::nullopt, ""s));
+        datastore.commitChanges();
+        DatastoreAccess::Tree expected {
+            {"/example-schema:dummy", empty_{}},
+        };
+        REQUIRE(datastore.getItems("/example-schema:dummy") == expected);
+    }
+
     SECTION("operational data")
     {
         MockDataSupplier mockOpsData;
diff --git a/tests/example-schema.yang b/tests/example-schema.yang
index 5d84eda..ef9d02d 100644
--- a/tests/example-schema.yang
+++ b/tests/example-schema.yang
@@ -243,4 +243,8 @@
     leaf blob {
         type binary;
     }
+
+    leaf dummy {
+        type empty;
+    }
 }
diff --git a/tests/leaf_editing.cpp b/tests/leaf_editing.cpp
index 64abbf2..666c975 100644
--- a/tests/leaf_editing.cpp
+++ b/tests/leaf_editing.cpp
@@ -70,7 +70,9 @@
         yang::TypeInfo{createEnum({"wlan0", "wlan1"})},
         yang::TypeInfo{yang::LeafRef{"/mod:portSettings/mod:port", std::make_unique<yang::TypeInfo>(schema->leafType("/mod:portSettings/mod:port"))}},
         yang::TypeInfo{yang::LeafRef{"/mod:activeMappedPort", std::make_unique<yang::TypeInfo>(schema->leafType("/mod:activeMappedPort"))}},
+        yang::TypeInfo{yang::Empty{}},
     }});
+    schema->addLeaf("/", "mod:dummy", yang::Empty{});
 
     Parser parser(schema);
     std::string input;
@@ -325,6 +327,10 @@
                             expected.m_data = enum_("utf3");
                         }
                     }
+                    SECTION("4. empty")
+                    {
+                        expected.m_data = empty_{};
+                    }
                 }
             }
 
@@ -448,6 +454,12 @@
                     expected.m_data = identityRef_{"pizza"};
                 }
             }
+            SECTION("empty")
+            {
+                input = "set mod:dummy ";
+                expected.m_path.m_nodes.push_back(dataNode_{module_{"mod"}, leaf_("dummy")});
+                expected.m_data = empty_{};
+            }
         }
 
         command_ command = parser.parseCommand(input, errorStream);
@@ -584,6 +596,11 @@
             input = "set mod:intOrString true";
         }
 
+        SECTION("no space for empty data")
+        {
+            input = "set mod:dummy";
+        }
+
         REQUIRE_THROWS_AS(parser.parseCommand(input, errorStream), InvalidCommandException);
         REQUIRE(errorStream.str().find(expectedError) != std::string::npos);
     }
diff --git a/tests/mock/sysrepo_subscription.cpp b/tests/mock/sysrepo_subscription.cpp
index 2c0d64b..0d4fe33 100644
--- a/tests/mock/sysrepo_subscription.cpp
+++ b/tests/mock/sysrepo_subscription.cpp
@@ -83,6 +83,11 @@
         v->set(xpath.c_str(), (what.m_prefix->m_name + what.m_value).c_str(), SR_IDENTITYREF_T);
     }
 
+    void operator()(const empty_)
+    {
+        v->set(xpath.c_str(), nullptr, SR_LEAF_EMPTY_T);
+    }
+
     void operator()(const std::string& what)
     {
         v->set(xpath.c_str(), what.c_str());
diff --git a/tests/yang.cpp b/tests/yang.cpp
index 009465b..79bad71 100644
--- a/tests/yang.cpp
+++ b/tests/yang.cpp
@@ -369,9 +369,14 @@
             type leafref {
                 path "../activeMappedPort";
             }
+            type empty;
         }
     }
 
+    leaf dummyLeaf {
+        type empty;
+    }
+
     leaf clockSpeed {
         type int64;
         config false;
@@ -800,6 +805,7 @@
                                 std::make_unique<yang::TypeInfo>(enums)
                         })
                     }},
+                    yang::TypeInfo{yang::Empty{}},
                 }};
             }
 
@@ -844,6 +850,7 @@
                         {"example-schema"s, "obsoleteLeafWithObsoleteType"},
                         {"example-schema"s, "myRpc"},
                         {"example-schema"s, "systemStats"},
+                        {"example-schema"s, "dummyLeaf"},
                         {"example-schema"s, "subLeaf"}};
                 }
 
@@ -893,6 +900,7 @@
                         {"example-schema"s, "clockSpeed"},
                         {"example-schema"s, "deprecatedLeaf"},
                         {"example-schema"s, "direction"},
+                        {"example-schema"s, "dummyLeaf"},
                         {"example-schema"s, "duration"},
                         {"example-schema"s, "ethernet"},
                         {"example-schema"s, "foodDrinkIdentLeaf"},
@@ -951,6 +959,7 @@
                         {boost::none, "/example-schema:deprecatedLeaf"},
                         {boost::none, "/example-schema:direction"},
                         {boost::none, "/example-schema:duration"},
+                        {boost::none, "/example-schema:dummyLeaf"},
                         {boost::none, "/example-schema:foodDrinkIdentLeaf"},
                         {boost::none, "/example-schema:foodIdentLeaf"},
                         {boost::none, "/example-schema:interface/caseEthernet/ethernet"},