Files
infix/package/libyang-cpp/0016-Add-a-helper-for-finding-opaque-nodes.patch

463 lines
16 KiB
Diff

From 8c59ecc5c687f8ee2ce62825835a378b422185f2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= <jan.kundrat@cesnet.cz>
Date: Fri, 14 Mar 2025 13:41:01 +0100
Subject: [PATCH 16/18] Add a helper for finding opaque nodes
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
This is needed for sysrepo where we want to search through the
sysrepo:discard-items top-level opaque nodes.
At first I wanted to simply wrap lyd_find_sibling_opaq_next(), but its
semantics is quite different to what is needed in the code which uses
sysrepo, so here's my attempt at a nice-ish API.
Change-Id: I2571961e42f6d7a121e27c881cacdcfec0e87762
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
include/libyang-cpp/DataNode.hpp | 2 +
src/DataNode.cpp | 49 ++++++
tests/data_node.cpp | 74 +++++++++
tests/yang/sysrepo@2024-10-25.yang | 257 +++++++++++++++++++++++++++++
4 files changed, 382 insertions(+)
create mode 100644 tests/yang/sysrepo@2024-10-25.yang
diff --git a/include/libyang-cpp/DataNode.hpp b/include/libyang-cpp/DataNode.hpp
index 310b5e3..50d6c0e 100644
--- a/include/libyang-cpp/DataNode.hpp
+++ b/include/libyang-cpp/DataNode.hpp
@@ -102,6 +102,7 @@ public:
bool isOpaque() const;
DataNodeOpaque asOpaque() const;
+ std::optional<DataNodeOpaque> firstOpaqueSibling() const;
// TODO: allow setting the `parent` argument
DataNode duplicate(const std::optional<DuplicationOptions> opts = std::nullopt) const;
@@ -241,6 +242,7 @@ struct LIBYANG_CPP_EXPORT OpaqueName {
std::optional<std::string> prefix;
std::string name;
std::string pretty() const;
+ bool matches(const std::string& prefixIsh, const std::string& name) const;
};
/**
diff --git a/src/DataNode.cpp b/src/DataNode.cpp
index 344f1b6..7e87917 100644
--- a/src/DataNode.cpp
+++ b/src/DataNode.cpp
@@ -1135,6 +1135,20 @@ std::string OpaqueName::pretty() const
}
}
+/**
+ * @short Fuzzy-match a real-world name against a combination of "something like a prefix" and "unqualified name"
+ *
+ * Because libyang doesn't propagate inherited prefixes, and because opaque nodes are magic, we seem to require
+ * this "fuzzy matching". It won't properly report a match on opaque nodes with a prefix that's inherited when
+ * using XML namespaces, though.
+ * */
+bool OpaqueName::matches(const std::string& prefixIsh, const std::string& name) const
+{
+ return name == this->name
+ && (prefixIsh == moduleOrNamespace
+ || (!!prefix && prefixIsh == *prefix));
+}
+
/**
* Wraps a raw non-null lyd_node pointer.
* @param node The pointer to be wrapped. Must not be null.
@@ -1305,4 +1319,39 @@ bool DataNode::siblingsEqual(const libyang::DataNode& other, const DataCompare f
}
}
+/**
+ * @short Find the first opaque node among the siblings
+ *
+ * This function was inspired by `lyd_find_sibling_opaq_next()`.
+ * */
+std::optional<DataNodeOpaque> DataNode::firstOpaqueSibling() const
+{
+ struct lyd_node *candidate = m_node;
+
+ // Skip all non-opaque nodes; libyang guarantees to have them first, followed by a (possibly empty) set
+ // of opaque nodes. This is not documented anywhere, but it was explicitly confirmed by the maintainer:
+ //
+ // JK: can I rely on all non-opaque nodes being listed first among the siblings, and then all opaque nodes
+ // in one continuous sequence (but with an unspecified order among the opaque nodes themselves)?
+ //
+ // MV: yep
+ while (candidate && candidate->schema) {
+ candidate = candidate->next;
+ }
+
+ // walk back through the opaque nodes; however, libyang lists are not your regular linked lists
+ while (candidate
+ && !candidate->prev->schema // don't go from the first opaque node through the non-opaque ones
+ && candidate->prev->next // don't wrap from the first node to the last one in case all of them are opaque
+ ) {
+ candidate = candidate->prev;
+ }
+
+ if (candidate) {
+ return DataNode{candidate, m_refs}.asOpaque();
+ }
+
+ return std::nullopt;
+}
+
}
diff --git a/tests/data_node.cpp b/tests/data_node.cpp
index a1096a6..db5a28e 100644
--- a/tests/data_node.cpp
+++ b/tests/data_node.cpp
@@ -1982,6 +1982,80 @@ TEST_CASE("Data Node manipulation")
"sysrepo:discard-items": "/example-schema:b"
}
)");
+
+ // check that a list which only consists of opaque nodes doesn't break our iteration
+ REQUIRE(!!discard1->firstOpaqueSibling());
+ REQUIRE(*discard1->firstOpaqueSibling() == *discard1);
+ REQUIRE(!!discard2->firstOpaqueSibling());
+ REQUIRE(*discard2->firstOpaqueSibling() == *discard1);
+
+ auto leafInt16 = ctx.newPath("/example-schema:leafInt16", "666");
+ leafInt16.insertSibling(*discard1);
+ REQUIRE(*discard1->firstSibling().printStr(libyang::DataFormat::JSON, libyang::PrintFlags::WithSiblings)
+ == R"({
+ "example-schema:leafInt16": 666,
+ "sysrepo:discard-items": "/example-schema:a",
+ "sysrepo:discard-items": "/example-schema:b"
+}
+)");
+ REQUIRE(leafInt16.firstSibling().path() == "/example-schema:leafInt16");
+ REQUIRE(discard1->firstSibling().path() == "/example-schema:leafInt16");
+ REQUIRE(discard2->firstSibling().path() == "/example-schema:leafInt16");
+
+ auto dummy = ctx.newPath("/example-schema:dummy", "blah");
+ auto opaqueLeaf = ctx.newPath("/example-schema:leafInt32", std::nullopt, libyang::CreationOptions::Opaque);
+ opaqueLeaf.newAttrOpaqueJSON("ietf-netconf", "operation", "delete");
+ dummy.insertSibling(opaqueLeaf);
+
+ // FIXME reword this: this one might not be handled by sysrepo, but we want it for our fuzzy matcher testing anyway
+ auto discard3 = ctx.newOpaqueXML(libyang::OpaqueName{"http://www.sysrepo.org/yang/sysrepo", "sysrepo", "discard-items"}, libyang::XML{"/example-schema:c"});
+ REQUIRE(!!discard3);
+ // notice that it's printed without a proper prefix at first...
+ REQUIRE(*discard3->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::Shrink)
+ == R"({"discard-items":"/example-schema:c"})");
+ // ...but after loading the module, the proper module is added back
+ ctx.parseModule(TESTS_DIR / "yang" / "sysrepo@2024-10-25.yang", libyang::SchemaFormat::YANG);
+ REQUIRE(*discard3->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::Shrink)
+ == R"({"sysrepo:discard-items":"/example-schema:c"})");
+
+ dummy.insertSibling(*discard3);
+ leafInt16.insertSibling(dummy);
+ REQUIRE(*discard1->firstSibling().printStr(libyang::DataFormat::JSON, libyang::PrintFlags::WithSiblings)
+ == R"({
+ "example-schema:dummy": "blah",
+ "example-schema:leafInt16": 666,
+ "sysrepo:discard-items": "/example-schema:a",
+ "sysrepo:discard-items": "/example-schema:b",
+ "example-schema:leafInt32": "",
+ "@example-schema:leafInt32": {
+ "ietf-netconf:operation": "delete"
+ },
+ "sysrepo:discard-items": "/example-schema:c"
+}
+)");
+
+ REQUIRE(dummy.firstOpaqueSibling() == discard1);
+ REQUIRE(dummy.firstOpaqueSibling() != discard2);
+ REQUIRE(leafInt16.firstOpaqueSibling() == discard1);
+ REQUIRE(opaqueLeaf.firstOpaqueSibling() == discard1);
+ REQUIRE(discard1->firstOpaqueSibling() == discard1);
+ REQUIRE(discard2->firstOpaqueSibling() == discard1);
+ REQUIRE(discard3->firstOpaqueSibling() == discard1);
+ REQUIRE(discard1->asOpaque().name().matches("sysrepo", "discard-items"));
+ REQUIRE(!discard1->asOpaque().name().matches("http://www.sysrepo.org/yang/sysrepo", "discard-items"));
+ REQUIRE(discard2->asOpaque().name().matches("sysrepo", "discard-items"));
+ REQUIRE(!discard2->asOpaque().name().matches("http://www.sysrepo.org/yang/sysrepo", "discard-items"));
+ REQUIRE(discard3->asOpaque().name().matches("sysrepo", "discard-items"));
+ REQUIRE(discard3->asOpaque().name().matches("http://www.sysrepo.org/yang/sysrepo", "discard-items"));
+ REQUIRE(!opaqueLeaf.asOpaque().name().matches("sysrepo", "discard-items"));
+
+ REQUIRE(!!dummy.firstOpaqueSibling()->nextSibling());
+ REQUIRE(dummy.firstOpaqueSibling()->nextSibling() == discard2);
+ REQUIRE(!!dummy.firstOpaqueSibling()->nextSibling()->nextSibling());
+ REQUIRE(dummy.firstOpaqueSibling()->nextSibling()->nextSibling() == opaqueLeaf);
+ REQUIRE(!!dummy.firstOpaqueSibling()->nextSibling()->nextSibling()->nextSibling());
+ REQUIRE(dummy.firstOpaqueSibling()->nextSibling()->nextSibling()->nextSibling() == discard3);
+ REQUIRE(!dummy.firstOpaqueSibling()->nextSibling()->nextSibling()->nextSibling()->nextSibling());
}
DOCTEST_SUBCASE("RESTCONF RPC output")
diff --git a/tests/yang/sysrepo@2024-10-25.yang b/tests/yang/sysrepo@2024-10-25.yang
new file mode 100644
index 0000000..f5bc32c
--- /dev/null
+++ b/tests/yang/sysrepo@2024-10-25.yang
@@ -0,0 +1,257 @@
+module sysrepo {
+ namespace "http://www.sysrepo.org/yang/sysrepo";
+ prefix sr;
+
+ yang-version 1.1;
+
+ import ietf-yang-types {
+ prefix yang;
+ }
+
+ import ietf-datastores {
+ prefix ds;
+ }
+
+ import ietf-yang-metadata {
+ prefix md;
+ revision-date 2016-08-05;
+ }
+
+ organization
+ "CESNET";
+
+ contact
+ "Author: Michal Vasko
+ <mvasko@cesnet.cz>";
+
+ description
+ "Sysrepo YANG datastore internal attributes and information.";
+
+ revision "2024-10-25" {
+ description
+ "Removed redundant metadata used for push operational data.";
+ }
+
+ revision "2019-07-10" {
+ description
+ "Initial revision.";
+ }
+
+ typedef module-ref {
+ description
+ "Reference to a module.";
+ type leafref {
+ path "/sysrepo-modules/module/name";
+ }
+ }
+
+ md:annotation operation {
+ type enumeration {
+ enum none {
+ description
+ "Node with this operation must exist but does not affect the datastore in any way.";
+ reference
+ "RFC 6241 section 7.2.: default-operation";
+ }
+ enum ether {
+ description
+ "Node with this operation does not have to exist and does not affect the datastore in any way.";
+ }
+ enum purge {
+ description
+ "Node with this operation represents an arbitrary generic node instance and all
+ the instances will be deleted.";
+ }
+ }
+ description
+ "Additional proprietary <edit-config> operations used internally.";
+ reference
+ "RFC 6241 section 7.2.";
+ }
+
+ identity notification {
+ base ds:datastore;
+ description
+ "Special datastore for storing notifications for replay.";
+ }
+
+ grouping module-info-grp {
+ leaf name {
+ type string;
+ description
+ "Module name.";
+ }
+
+ leaf revision {
+ type string;
+ description
+ "Module revision.";
+ }
+
+ leaf-list enabled-feature {
+ type string;
+ description
+ "List of all the enabled features.";
+ }
+
+ list plugin {
+ key "datastore";
+ description
+ "Module datastore plugin handling specific datastore data.";
+
+ leaf datastore {
+ type identityref {
+ base ds:datastore;
+ }
+ description
+ "Datastore of this plugin.";
+ }
+
+ leaf name {
+ type string;
+ mandatory true;
+ description
+ "Specific plugin name as present in the plugin structures.";
+ }
+ }
+ }
+
+ grouping deps-grp {
+ list lref {
+ description
+ "Dependency of a leafref node.";
+
+ leaf target-path {
+ type yang:xpath1.0;
+ mandatory true;
+ description
+ "Path identifying the leafref target node.";
+ }
+
+ leaf target-module {
+ type module-ref;
+ mandatory true;
+ description
+ "Foreign target module of the leafref.";
+ }
+ }
+
+ list inst-id {
+ description
+ "Dependency of an instance-identifier node.";
+
+ leaf source-path {
+ type yang:xpath1.0;
+ mandatory true;
+ description
+ "Path identifying the instance-identifier node.";
+ }
+
+ leaf default-target-path {
+ type yang:xpath1.0;
+ description
+ "Default instance-identifier value.";
+ }
+ }
+
+ list xpath {
+ description
+ "Dependency of an XPath expression - must or when statement.";
+
+ leaf expression {
+ type yang:xpath1.0;
+ mandatory true;
+ description
+ "XPath expression of the dependency - must or when statement argument.";
+ }
+
+ leaf-list target-module {
+ type module-ref;
+ description
+ "Foreign modules with the data needed for evaluation of the XPath.";
+ }
+ }
+ }
+
+ container sysrepo-modules {
+ config false;
+ description
+ "All installed Sysrepo modules.";
+
+ leaf content-id {
+ type uint32;
+ mandatory true;
+ description
+ "Sysrepo module-set content-id to be used for its generated yang-library data.";
+ }
+
+ list module {
+ key "name";
+ description
+ "Sysrepo module.";
+
+ uses module-info-grp;
+
+ leaf replay-support {
+ type yang:date-and-time;
+ description
+ "Present only if the module supports replay. Means the earliest stored notification if any present.
+ Otherwise the time the replay support was switched on.";
+ }
+
+ container deps {
+ description
+ "Module data dependencies on other modules.";
+ uses deps-grp;
+ }
+
+ leaf-list inverse-deps {
+ type module-ref;
+ description
+ "List of modules that depend on this module.";
+ }
+
+ list rpc {
+ key "path";
+ description
+ "Module RPC/actions.";
+
+ leaf path {
+ type yang:xpath1.0;
+ description
+ "Path identifying the operation.";
+ }
+
+ container in {
+ description
+ "Operation input dependencies.";
+ uses deps-grp;
+ }
+
+ container out {
+ description
+ "Operation output dependencies.";
+ uses deps-grp;
+ }
+ }
+
+ list notification {
+ key "path";
+ description
+ "Module notifications.";
+
+ leaf path {
+ type yang:xpath1.0;
+ description
+ "Path identifying the notification.";
+ }
+
+ container deps {
+ description
+ "Notification dependencies.";
+ uses deps-grp;
+ }
+ }
+ }
+ }
+}
--
2.43.0