From 22442d322bda3d83457dd0f1da45d95a10e93081 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= <jan.kundrat@cesnet.cz>
Date: Thu, 13 Mar 2025 17:32:59 +0100
Subject: [PATCH 30/44] Always print empty containers in user-defined content
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires

There's some subtle interaction between depth-limited printing, default
values, and empty non-presence containers. In order to prepare for an
upstream change in sysrepo which fixed depth-limited printing, let's
stick to "always" printing the empty non-presence containers.

(It actually isn't "always" because the data such as error reports,
patches, etc., are still printed using the default settings. But the
data which is subject to user-defined constraints is now consistent.)

Change-Id: Ie2d6ad2e851d667e7e7582fcd0ac75dabce2c05e
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
 src/restconf/Server.cpp     |  4 +-
 src/restconf/utils/yang.cpp |  2 +-
 src/sr/AllEvents.cpp        |  2 +-
 src/sr/OpticalEvents.cpp    |  2 +-
 tests/restconf-defaults.cpp | 12 +++++-
 tests/restconf-nacm.cpp     | 12 +++++-
 tests/restconf-reading.cpp  | 79 ++++++++++++++++++++++++++++++++++---
 7 files changed, 98 insertions(+), 15 deletions(-)

diff --git a/src/restconf/Server.cpp b/src/restconf/Server.cpp
index 55e504a..bae3992 100644
--- a/src/restconf/Server.cpp
+++ b/src/restconf/Server.cpp
@@ -478,7 +478,7 @@ void processActionOrRPC(std::shared_ptr<RequestContext> requestCtx, const std::c
                                         contentType(requestCtx->dataFormat.response),
                                         CORS,
                                     });
-    requestCtx->res.end(*envelope->printStr(requestCtx->dataFormat.response, libyang::PrintFlags::WithSiblings));
+    requestCtx->res.end(*envelope->printStr(requestCtx->dataFormat.response, libyang::PrintFlags::WithSiblings | libyang::PrintFlags::KeepEmptyCont));
 }
 
 void processPost(std::shared_ptr<RequestContext> requestCtx, const std::chrono::milliseconds timeout)
@@ -768,7 +768,7 @@ libyang::PrintFlags libyangPrintFlags(const libyang::DataNode& dataNode, const s
     } catch(const libyang::Error& e) {
     }
 
-    libyang::PrintFlags ret = libyang::PrintFlags::WithSiblings;
+    libyang::PrintFlags ret = libyang::PrintFlags::WithSiblings | libyang::PrintFlags::KeepEmptyCont;
 
     if (!withDefaults && node && (node->schema().nodeType() == libyang::NodeType::Leaf || node->schema().nodeType() == libyang::NodeType::Leaflist) && node->asTerm().isImplicitDefault()) {
         return ret | libyang::PrintFlags::WithDefaultsAll;
diff --git a/src/restconf/utils/yang.cpp b/src/restconf/utils/yang.cpp
index 30661fc..8acb160 100644
--- a/src/restconf/utils/yang.cpp
+++ b/src/restconf/utils/yang.cpp
@@ -111,7 +111,7 @@ std::string as_restconf_notification(const libyang::Context& ctx, libyang::DataF
     envelope->insertChild(*eventTime);
     envelope->insertChild(notification);
 
-    auto res = *envelope->printStr(dataFormat, libyang::PrintFlags::WithSiblings);
+    auto res = *envelope->printStr(dataFormat, libyang::PrintFlags::WithSiblings | libyang::PrintFlags::KeepEmptyCont);
 
     // notification node comes from sysrepo and sysrepo will free this; if not unlinked then envelope destructor would try to free this as well
     notification.unlink();
diff --git a/src/sr/AllEvents.cpp b/src/sr/AllEvents.cpp
index 6e2a21c..2aca4c7 100644
--- a/src/sr/AllEvents.cpp
+++ b/src/sr/AllEvents.cpp
@@ -109,7 +109,7 @@ sysrepo::ErrorCode AllEvents::onChange(sysrepo::Session session, const std::stri
                 break;
             }
         };
-        auto json = *copy.printStr(libyang::DataFormat::JSON, libyang::PrintFlags::WithSiblings);
+        auto json = *copy.printStr(libyang::DataFormat::JSON, libyang::PrintFlags::WithSiblings | libyang::PrintFlags::KeepEmptyCont);
         spdlog::info("JSON: {}", json);
         spdlog::warn("FULL JSON: {}",
                 *session.getData('/' + module + ":*")->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::WithSiblings));
diff --git a/src/sr/OpticalEvents.cpp b/src/sr/OpticalEvents.cpp
index 8c5feff..25906a8 100644
--- a/src/sr/OpticalEvents.cpp
+++ b/src/sr/OpticalEvents.cpp
@@ -15,7 +15,7 @@
 namespace {
 std::string dumpDataFrom(sysrepo::Session session, const std::string& module)
 {
-    return *session.getData('/' + module + ":*")->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::WithSiblings);
+    return *session.getData('/' + module + ":*")->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::WithSiblings | libyang::PrintFlags::KeepEmptyCont);
 }
 }
 
diff --git a/tests/restconf-defaults.cpp b/tests/restconf-defaults.cpp
index d5c6004..89083f1 100644
--- a/tests/restconf-defaults.cpp
+++ b/tests/restconf-defaults.cpp
@@ -33,7 +33,11 @@ TEST_CASE("default handling")
 
     // default value of /example:a/b/c/enabled is implicitly set so it should not be printed
     REQUIRE(get(RESTCONF_DATA_ROOT "/example:a/b/c", {}) == Response{200, jsonHeaders, R"({
-
+  "example:a": {
+    "b": {
+      "c": {}
+    }
+  }
 }
 )"});
 
@@ -88,7 +92,11 @@ TEST_CASE("default handling")
 
     // default value is only there implicitly, so it should *not* be printed
     REQUIRE(get(RESTCONF_DATA_ROOT "/example:a/b/c", {}) == Response{200, jsonHeaders, R"({
-
+  "example:a": {
+    "b": {
+      "c": {}
+    }
+  }
 }
 )"});
 }
diff --git a/tests/restconf-nacm.cpp b/tests/restconf-nacm.cpp
index 29d7723..51328f3 100644
--- a/tests/restconf-nacm.cpp
+++ b/tests/restconf-nacm.cpp
@@ -130,6 +130,9 @@ TEST_CASE("NACM")
     "clock": {
       "timezone-utc-offset": 2
     },
+    "dns-resolver": {
+      "options": {}
+    },
     "radius": {
       "server": [
         {
@@ -139,7 +142,8 @@ TEST_CASE("NACM")
             "shared-secret": "shared-secret"
           }
         }
-      ]
+      ],
+      "options": {}
     }
   }
 }
@@ -207,6 +211,9 @@ TEST_CASE("NACM")
     "clock": {
       "timezone-utc-offset": 2
     },
+    "dns-resolver": {
+      "options": {}
+    },
     "radius": {
       "server": [
         {
@@ -216,7 +223,8 @@ TEST_CASE("NACM")
             "shared-secret": "shared-secret"
           }
         }
-      ]
+      ],
+      "options": {}
     }
   }
 }
diff --git a/tests/restconf-reading.cpp b/tests/restconf-reading.cpp
index e709486..ffdb047 100644
--- a/tests/restconf-reading.cpp
+++ b/tests/restconf-reading.cpp
@@ -62,10 +62,22 @@ TEST_CASE("reading data")
         // this relies on a NACM rule for anonymous access that filters out "a lot of stuff"
         REQUIRE(get(RESTCONF_DATA_ROOT, {}) == Response{200, jsonHeaders, R"({
   "example:top-level-leaf": "moo",
+  "example:tlc": {},
+  "example:a": {
+    "b": {
+      "c": {}
+    },
+    "b1": {},
+    "example-augment:b": {
+      "c": {}
+    }
+  },
+  "example:two-leafs": {},
   "example:config-nonconfig": {
     "config-node": "foo-config-true",
     "nonconfig-node": "foo-config-false"
   },
+  "example:ordered-lists": {},
   "ietf-restconf-monitoring:restconf-state": {
     "capabilities": {
       "capability": [
@@ -107,10 +119,22 @@ TEST_CASE("reading data")
 
         REQUIRE(get(RESTCONF_ROOT_DS("operational"), {}) == Response{200, jsonHeaders, R"({
   "example:top-level-leaf": "moo",
+  "example:tlc": {},
+  "example:a": {
+    "b": {
+      "c": {}
+    },
+    "b1": {},
+    "example-augment:b": {
+      "c": {}
+    }
+  },
+  "example:two-leafs": {},
   "example:config-nonconfig": {
     "config-node": "foo-config-true",
     "nonconfig-node": "foo-config-false"
   },
+  "example:ordered-lists": {},
   "ietf-restconf-monitoring:restconf-state": {
     "capabilities": {
       "capability": [
@@ -152,9 +176,21 @@ TEST_CASE("reading data")
 
         REQUIRE(get(RESTCONF_ROOT_DS("running"), {}) == Response{200, jsonHeaders, R"({
   "example:top-level-leaf": "moo",
+  "example:tlc": {},
+  "example:a": {
+    "b": {
+      "c": {}
+    },
+    "b1": {},
+    "example-augment:b": {
+      "c": {}
+    }
+  },
+  "example:two-leafs": {},
   "example:config-nonconfig": {
     "config-node": "foo-config-true"
-  }
+  },
+  "example:ordered-lists": {}
 }
 )"});
     }
@@ -213,7 +249,8 @@ TEST_CASE("reading data")
         {
           "name": "a"
         }
-      ]
+      ],
+      "options": {}
     }
   }
 }
@@ -244,7 +281,8 @@ TEST_CASE("reading data")
             "shared-secret": "shared-secret"
           }
         }
-      ]
+      ],
+      "options": {}
     }
   }
 }
@@ -714,6 +752,7 @@ TEST_CASE("reading data")
         "enabled": true
       }
     },
+    "b1": {},
     "example-augment:b": {
       "c": {
         "enabled": true
@@ -723,9 +762,18 @@ TEST_CASE("reading data")
 }
 )"});
             REQUIRE(get(RESTCONF_DATA_ROOT "/example:a?with-defaults=explicit", {}) == Response{200, jsonHeaders, R"({
-
+  "example:a": {
+    "b": {
+      "c": {}
+    },
+    "b1": {},
+    "example-augment:b": {
+      "c": {}
+    }
+  }
 }
 )"});
+            // FIXME: libyang is not really consistent in printing of NP-containers when trimming away the defaults...
             REQUIRE(get(RESTCONF_DATA_ROOT "/example:a?with-defaults=trim", {}) == Response{200, jsonHeaders, R"({
 
 }
@@ -740,6 +788,7 @@ TEST_CASE("reading data")
         }
       }
     },
+    "b1": {},
     "example-augment:b": {
       "c": {
         "enabled": true,
@@ -766,6 +815,7 @@ TEST_CASE("reading data")
         "enabled": true
       }
     },
+    "b1": {},
     "example-augment:b": {
       "c": {
         "enabled": true
@@ -781,13 +831,24 @@ TEST_CASE("reading data")
       "c": {
         "enabled": true
       }
+    },
+    "b1": {},
+    "example-augment:b": {
+      "c": {}
     }
   }
 }
 )"});
 
+            // FIXME: libyang is not consistent here:
+            // - /example:a/b/c NP-container *is* printed,
+            // - /example:a/example-augment:b/c NP-container is *not* printed
             REQUIRE(get(RESTCONF_DATA_ROOT "/example:a?with-defaults=trim", {}) == Response{200, jsonHeaders, R"({
-
+  "example:a": {
+    "b": {
+      "c": {}
+    }
+  }
 }
 )"});
 
@@ -801,6 +862,7 @@ TEST_CASE("reading data")
         }
       }
     },
+    "b1": {},
     "example-augment:b": {
       "c": {
         "enabled": true,
@@ -830,10 +892,15 @@ TEST_CASE("reading data")
 )"});
 
         REQUIRE(get(RESTCONF_DATA_ROOT "/example:a/b/c/enabled?with-defaults=explicit", {}) == Response{200, jsonHeaders, R"({
-
+  "example:a": {
+    "b": {
+      "c": {}
+    }
+  }
 }
 )"});
 
+        // again, libyang is not 100% consistent in the `trim` mode
         REQUIRE(get(RESTCONF_DATA_ROOT "/example:a/b/c/enabled?with-defaults=trim", {}) == Response{200, jsonHeaders, R"({
 
 }
-- 
2.43.0

