From f958af42bf5d9fbd901ed59ebc1359ac0ddcc00f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= <jan.kundrat@cesnet.cz>
Date: Fri, 14 Mar 2025 11:36:50 +0100
Subject: [PATCH 15/18] API/ABI change: opaque node naming
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires

Our C++ API would ignore the "module name or XML prefix", which turns
out to be *the* relevant part when it comes to opaque node naming. The
prefix is, instead, just that string that might have been inherited from
the parent node when parsing the serialized data; it's an optional
thingy which, if not set explicitly, is implicitly inherited.

Adapt the API for this, and since this *will* break the build, let's
bump the package version.

Change-Id: I199afe5fa7a571034b744531c63b93b9c656563a
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
 CMakeLists.txt                   |  5 ++---
 include/libyang-cpp/Context.hpp  |  4 ++--
 include/libyang-cpp/DataNode.hpp | 11 ++++++++--
 src/Context.cpp                  | 32 ++++++++++++++++++++--------
 src/DataNode.cpp                 | 19 ++++++++++++++---
 tests/data_node.cpp              | 36 ++++++++++++++++++++++++++------
 6 files changed, 82 insertions(+), 25 deletions(-)

diff --git a/CMakeLists.txt b/CMakeLists.txt
index a40fd52..c5fec45 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -21,7 +21,7 @@ add_custom_target(libyang-cpp-version-cmake
         cmake/ProjectGitVersionRunner.cmake
         )
 include(cmake/ProjectGitVersion.cmake)
-set(LIBYANG_CPP_PKG_VERSION "3")
+set(LIBYANG_CPP_PKG_VERSION "4")
 prepare_git_version(LIBYANG_CPP_VERSION ${LIBYANG_CPP_PKG_VERSION})
 
 find_package(Doxygen)
@@ -29,8 +29,7 @@ option(WITH_DOCS "Create and install internal documentation (needs Doxygen)" ${D
 option(BUILD_SHARED_LIBS "By default, shared libs are enabled. Turn off for a static build." ON)
 
 find_package(PkgConfig REQUIRED)
-# FIXME: it's actually 3.7.12, but that hasn't been released yet
-pkg_check_modules(LIBYANG REQUIRED libyang>=3.7.11 IMPORTED_TARGET)
+pkg_check_modules(LIBYANG REQUIRED libyang>=3.10.1 IMPORTED_TARGET)
 
 # FIXME from gcc 14.1 on we should be able to use the calendar/time from libstdc++ and thus remove the date dependency
 find_package(date)
diff --git a/include/libyang-cpp/Context.hpp b/include/libyang-cpp/Context.hpp
index baa47b3..ca89063 100644
--- a/include/libyang-cpp/Context.hpp
+++ b/include/libyang-cpp/Context.hpp
@@ -115,8 +115,8 @@ public:
     CreatedNodes newPath2(const std::string& path, libyang::JSON json, const std::optional<CreationOptions> options = std::nullopt) const;
     CreatedNodes newPath2(const std::string& path, libyang::XML xml, const std::optional<CreationOptions> options = std::nullopt) const;
     std::optional<DataNode> newExtPath(const ExtensionInstance& ext, const std::string& path, const std::optional<std::string>& value, const std::optional<CreationOptions> options = std::nullopt) const;
-    std::optional<DataNode> newOpaqueJSON(const std::string& moduleName, const std::string& name, const std::optional<libyang::JSON>& value) const;
-    std::optional<DataNode> newOpaqueXML(const std::string& moduleName, const std::string& name, const std::optional<libyang::XML>& value) const;
+    std::optional<DataNode> newOpaqueJSON(const OpaqueName& name, const std::optional<libyang::JSON>& value) const;
+    std::optional<DataNode> newOpaqueXML(const OpaqueName& name, const std::optional<libyang::XML>& value) const;
     SchemaNode findPath(const std::string& dataPath, const InputOutputNodes inputOutputNodes = InputOutputNodes::Input) const;
     Set<SchemaNode> findXPath(const std::string& path) const;
 
diff --git a/include/libyang-cpp/DataNode.hpp b/include/libyang-cpp/DataNode.hpp
index 851681b..310b5e3 100644
--- a/include/libyang-cpp/DataNode.hpp
+++ b/include/libyang-cpp/DataNode.hpp
@@ -225,15 +225,22 @@ private:
 };
 
 /**
- * @brief Contains a (possibly module-qualified) name of an opaque node.
+ * @brief Contains a name of an opaque node.
  *
- * This is generic container of a prefix/module string and a name string.
+ * An opaque node always has a name, and a module (or XML namespace) to which this node belongs.
+ * Sometimes, it also has a prefix.
+ *
+ * If the prefix is set *and* the underlying node is an opaque JSON one, then the prefix must be the same as the "module or namespace" name.
+ * If the underlying node is an opaque XML one, then the XML prefix might be something completely different, and in that case the real fun begins.
+ * Review the libayng C manual, this is something that the C++ wrapper doesn't really have under control.
  *
  * Wraps `ly_opaq_name`.
  */
 struct LIBYANG_CPP_EXPORT OpaqueName {
+    std::string moduleOrNamespace;
     std::optional<std::string> prefix;
     std::string name;
+    std::string pretty() const;
 };
 
 /**
diff --git a/src/Context.cpp b/src/Context.cpp
index 287f8c8..fec2f27 100644
--- a/src/Context.cpp
+++ b/src/Context.cpp
@@ -378,17 +378,25 @@ std::optional<DataNode> Context::newExtPath(const ExtensionInstance& ext, const
  *
  * Wraps `lyd_new_opaq`.
  *
- * @param moduleName Node module name, used as a prefix as well
  * @param name Name of the created node
  * @param value JSON data blob, if any
  * @return Returns the newly created node (if created)
  */
-std::optional<DataNode> Context::newOpaqueJSON(const std::string& moduleName, const std::string& name, const std::optional<libyang::JSON>& value) const
+std::optional<DataNode> Context::newOpaqueJSON(const OpaqueName& name, const std::optional<libyang::JSON>& value) const
 {
+    if (name.prefix && *name.prefix != name.moduleOrNamespace) {
+        throw Error{"invalid opaque JSON node: prefix \"" + *name.prefix + "\" doesn't match module name \"" + name.moduleOrNamespace + "\""};
+    }
     lyd_node* out;
-    auto err = lyd_new_opaq(nullptr, m_ctx.get(), name.c_str(), value ? value->content.c_str() : nullptr, nullptr, moduleName.c_str(), &out);
+    auto err = lyd_new_opaq(nullptr,
+                            m_ctx.get(),
+                            name.name.c_str(),
+                            value ? value->content.c_str() : nullptr,
+                            name.prefix ? name.prefix->c_str() : nullptr,
+                            name.moduleOrNamespace.c_str(),
+                            &out);
 
-    throwIfError(err, "Couldn't create an opaque JSON node '"s + moduleName + ':' + name + "'");
+    throwIfError(err, "Couldn't create an opaque JSON node " + name.pretty());
 
     if (out) {
         return DataNode{out, std::make_shared<internal_refcount>(m_ctx)};
@@ -403,17 +411,23 @@ std::optional<DataNode> Context::newOpaqueJSON(const std::string& moduleName, co
  *
  * Wraps `lyd_new_opaq2`.
  *
- * @param xmlNamespace Node module namespace
  * @param name Name of the created node
  * @param value XML data blob, if any
  * @return Returns the newly created node (if created)
  */
-std::optional<DataNode> Context::newOpaqueXML(const std::string& xmlNamespace, const std::string& name, const std::optional<libyang::XML>& value) const
+std::optional<DataNode> Context::newOpaqueXML(const OpaqueName& name, const std::optional<libyang::XML>& value) const
 {
+    // the XML node naming is "complex", we cannot really check the XML namespace for sanity here
     lyd_node* out;
-    auto err = lyd_new_opaq2(nullptr, m_ctx.get(), name.c_str(), value ? value->content.c_str() : nullptr, nullptr, xmlNamespace.c_str(), &out);
-
-    throwIfError(err, "Couldn't create an opaque XML node '"s + name +"' from namespace '" + xmlNamespace + "'");
+    auto err = lyd_new_opaq2(nullptr,
+                             m_ctx.get(),
+                             name.name.c_str(),
+                             value ? value->content.c_str() : nullptr,
+                             name.prefix ? name.prefix->c_str() : nullptr,
+                             name.moduleOrNamespace.c_str(),
+                             &out);
+
+    throwIfError(err, "Couldn't create an opaque XML node " + name.pretty());
 
     if (out) {
         return DataNode{out, std::make_shared<internal_refcount>(m_ctx)};
diff --git a/src/DataNode.cpp b/src/DataNode.cpp
index b899b18..344f1b6 100644
--- a/src/DataNode.cpp
+++ b/src/DataNode.cpp
@@ -1112,9 +1112,9 @@ OpaqueName DataNodeOpaque::name() const
 {
     auto opaq = reinterpret_cast<lyd_node_opaq*>(m_node);
     return OpaqueName{
-        .prefix = opaq->name.prefix ? std::optional(opaq->name.prefix) : std::nullopt,
-        .name = opaq->name.name
-    };
+        .moduleOrNamespace = opaq->name.module_name,
+        .prefix = opaq->name.prefix ? std::optional{opaq->name.prefix} : std::nullopt,
+        .name = opaq->name.name};
 }
 
 std::string DataNodeOpaque::value() const
@@ -1122,6 +1122,19 @@ std::string DataNodeOpaque::value() const
     return reinterpret_cast<lyd_node_opaq*>(m_node)->value;
 }
 
+std::string OpaqueName::pretty() const
+{
+    if (prefix) {
+        if (*prefix == moduleOrNamespace) {
+            return *prefix + ':' + name;
+        } else {
+            return "{" + moduleOrNamespace + "}, " + *prefix + ':' + name;
+        }
+    } else {
+        return "{" + moduleOrNamespace + "}, " + name;
+    }
+}
+
 /**
  * Wraps a raw non-null lyd_node pointer.
  * @param node The pointer to be wrapped. Must not be null.
diff --git a/tests/data_node.cpp b/tests/data_node.cpp
index b6ee455..a1096a6 100644
--- a/tests/data_node.cpp
+++ b/tests/data_node.cpp
@@ -1969,9 +1969,11 @@ TEST_CASE("Data Node manipulation")
 
         DOCTEST_SUBCASE("opaque nodes for sysrepo ops data discard")
         {
-            auto discard1 = ctx.newOpaqueJSON("sysrepo", "discard-items", libyang::JSON{"/example-schema:a"});
+            // the "short" form with no prefix
+            auto discard1 = ctx.newOpaqueJSON(libyang::OpaqueName{"sysrepo", std::nullopt, "discard-items"}, libyang::JSON{"/example-schema:a"});
             REQUIRE(!!discard1);
-            auto discard2 = ctx.newOpaqueJSON("sysrepo", "discard-items", libyang::JSON{"/example-schema:b"});
+            // let's use a prefix form here
+            auto discard2 = ctx.newOpaqueJSON(libyang::OpaqueName{"sysrepo", "sysrepo", "discard-items"}, libyang::JSON{"/example-schema:b"});
             REQUIRE(!!discard2);
             discard1->insertSibling(*discard2);
             REQUIRE(*discard1->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::WithSiblings)
@@ -2001,16 +2003,38 @@ TEST_CASE("Data Node manipulation")
             auto data = ctx.newPath2("/example-schema:myRpc/outputLeaf", "AHOJ", libyang::CreationOptions::Output).createdNode;
             REQUIRE(data);
             data->newPath("/example-schema:myRpc/another", "yay", libyang::CreationOptions::Output);
+            std::string prettyName;
 
-            DOCTEST_SUBCASE("JSON") {
-                out = ctx.newOpaqueJSON(data->schema().module().name(), "output", std::nullopt);
+            DOCTEST_SUBCASE("JSON no prefix") {
+                out = ctx.newOpaqueJSON({data->schema().module().name(), std::nullopt, "output"}, std::nullopt);
+                prettyName = "{example-schema}, output";
             }
 
-            DOCTEST_SUBCASE("XML") {
-                out = ctx.newOpaqueXML(data->schema().module().ns(), "output", std::nullopt);
+            DOCTEST_SUBCASE("JSON with prefix") {
+                out = ctx.newOpaqueJSON({data->schema().module().name(), data->schema().module().name(), "output"}, std::nullopt);
+                prettyName = "example-schema:output";
+
+                // wrong prefix is detected
+                REQUIRE_THROWS_WITH_AS(ctx.newOpaqueJSON({data->schema().module().name(), "xxx", "output"}, std::nullopt),
+                                       R"(invalid opaque JSON node: prefix "xxx" doesn't match module name "example-schema")",
+                                       libyang::Error);
+            }
+
+            DOCTEST_SUBCASE("XML no prefix") {
+                out = ctx.newOpaqueXML({data->schema().module().ns(), std::nullopt, "output"}, std::nullopt);
+                prettyName = "{http://example.com/coze}, output";
+            }
+
+            DOCTEST_SUBCASE("XML with prefix") {
+                out = ctx.newOpaqueXML({data->schema().module().ns(),
+                                        data->schema().module().name() /* prefix is a module name, not the XML NS*/,
+                                        "output"},
+                                       std::nullopt);
+                prettyName = "{http://example.com/coze}, example-schema:output";
             }
 
             REQUIRE(out);
+            REQUIRE(prettyName == out->asOpaque().name().pretty());
             data->unlinkWithSiblings();
             out->insertChild(*data);
 
-- 
2.43.0

