From 8c59ecc5c687f8ee2ce62825835a378b422185f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= 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 --- 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 firstOpaqueSibling() const; // TODO: allow setting the `parent` argument DataNode duplicate(const std::optional opts = std::nullopt) const; @@ -241,6 +242,7 @@ struct LIBYANG_CPP_EXPORT OpaqueName { std::optional 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 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 + "; + + 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 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