Bump sysrepo, netopeer,libyang and libnetconf2, libyang-cpp, sysrepo-cpp, rousette

This commit is contained in:
Mattias Walström
2025-09-17 08:51:31 +02:00
parent 197bb1507e
commit e7f68be143
118 changed files with 2498 additions and 24290 deletions
@@ -0,0 +1,135 @@
From 22b41e7ed8268b94ccc139138e2037c390a3a616 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bed=C5=99ich=20Schindler?= <bedrich.schindler@gmail.com>
Date: Thu, 10 Jul 2025 15:00:45 +0200
Subject: [PATCH] Implement `SchemaNode::actionRpcs()`
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
This function allows to access list of actions
within schema node.
It is implemented same as `Module::actionRpcs`
to follow same approach, so there is no need to use
collections.
Change-Id: Ie99908cfdd334433fd9ae6f1a909f196e61139fd
Signed-off-by: Jan Kundrát <jan.kundrat@cesnet.cz>
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
include/libyang-cpp/SchemaNode.hpp | 1 +
src/SchemaNode.cpp | 12 ++++++++++++
tests/example_schema.hpp | 28 ++++++++++++++++++++++++++++
tests/schema_node.cpp | 21 +++++++++++++++++++++
4 files changed, 62 insertions(+)
diff --git a/include/libyang-cpp/SchemaNode.hpp b/include/libyang-cpp/SchemaNode.hpp
index 0f1a4c4..9f49fd7 100644
--- a/include/libyang-cpp/SchemaNode.hpp
+++ b/include/libyang-cpp/SchemaNode.hpp
@@ -79,6 +79,7 @@ public:
Collection<SchemaNode, IterationType::Sibling> siblings() const;
Collection<SchemaNode, IterationType::Sibling> immediateChildren() const;
std::vector<ExtensionInstance> extensionInstances() const;
+ std::vector<SchemaNode> actionRpcs() const;
std::vector<When> when() const;
diff --git a/src/SchemaNode.cpp b/src/SchemaNode.cpp
index 26b5099..ce24203 100644
--- a/src/SchemaNode.cpp
+++ b/src/SchemaNode.cpp
@@ -117,6 +117,18 @@ Collection<SchemaNode, IterationType::Sibling> SchemaNode::immediateChildren() c
return c ? c->siblings() : Collection<SchemaNode, IterationType::Sibling>{nullptr, nullptr};
}
+/**
+ * @brief Returns a collection of action nodes (not RPC nodes) as SchemaNode
+ */
+std::vector<SchemaNode> SchemaNode::actionRpcs() const
+{
+ std::vector<SchemaNode> res;
+ for (auto action = reinterpret_cast<const struct lysc_node*>(lysc_node_actions(m_node)); action; action = action->next) {
+ res.emplace_back(SchemaNode{action, m_ctx});
+ }
+ return res;
+}
+
/**
* Returns the YANG description of the node.
*
diff --git a/tests/example_schema.hpp b/tests/example_schema.hpp
index 0d8acb9..02d3d74 100644
--- a/tests/example_schema.hpp
+++ b/tests/example_schema.hpp
@@ -214,6 +214,34 @@ module example-schema {
}
container bigTree {
+ action firstAction {
+ input {
+ leaf inputLeaf1 {
+ type string;
+ }
+ }
+
+ output {
+ leaf outputLeaf1 {
+ type string;
+ }
+ }
+ }
+
+ action secondAction {
+ input {
+ leaf inputLeaf2 {
+ type string;
+ }
+ }
+
+ output {
+ leaf outputLeaf2 {
+ type string;
+ }
+ }
+ }
+
container one {
leaf myLeaf {
type string;
diff --git a/tests/schema_node.cpp b/tests/schema_node.cpp
index 0001377..65ef462 100644
--- a/tests/schema_node.cpp
+++ b/tests/schema_node.cpp
@@ -544,6 +544,27 @@ TEST_CASE("SchemaNode")
REQUIRE(elem.extensionInstances()[2].argument() == "last-modified");
}
+ DOCTEST_SUBCASE("SchemaNode::actionRpcs")
+ {
+ DOCTEST_SUBCASE("no actions")
+ {
+ auto actions = ctx->findPath("/example-schema:presenceContainer").actionRpcs();
+ REQUIRE(actions.size() == 0);
+ }
+
+ DOCTEST_SUBCASE("two actions")
+ {
+ auto actions = ctx->findPath("/example-schema:bigTree").actionRpcs();
+ REQUIRE(actions.size() == 2);
+ REQUIRE(actions[0].asActionRpc().name() == "firstAction");
+ REQUIRE(actions[0].asActionRpc().input().child()->name() == "inputLeaf1");
+ REQUIRE(actions[0].asActionRpc().output().child()->name() == "outputLeaf1");
+ REQUIRE(actions[1].asActionRpc().name() == "secondAction");
+ REQUIRE(actions[1].asActionRpc().input().child()->name() == "inputLeaf2");
+ REQUIRE(actions[1].asActionRpc().output().child()->name() == "outputLeaf2");
+ }
+ }
+
DOCTEST_SUBCASE("SchemaNode::operator==")
{
auto a = ctx->findPath("/type_module:leafString");
--
2.43.0
@@ -1,82 +0,0 @@
From d0f6422fee7a46fcb7445c88f499f61b3eb0ead0 Mon Sep 17 00:00:00 2001
From: Adam Piecek <Adam.Piecek@cesnet.cz>
Date: Wed, 23 Oct 2024 14:37:09 +0200
Subject: [PATCH 01/18] added support for RpcYang in Context::parseOp
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
Change-Id: I25182ea2d042be1e6e4246e18aee260cc032e547
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
src/Context.cpp | 6 ++++--
tests/context.cpp | 21 +++++++++++++++++++++
2 files changed, 25 insertions(+), 2 deletions(-)
diff --git a/src/Context.cpp b/src/Context.cpp
index b844bd9..287f8c8 100644
--- a/src/Context.cpp
+++ b/src/Context.cpp
@@ -221,7 +221,8 @@ std::optional<DataNode> Context::parseExtData(
* - a NETCONF RPC,
* - a NETCONF notification,
* - a RESTCONF notification,
- * - a YANG notification.
+ * - a YANG notification,
+ * - a YANG RPC.
*
* Parsing any of these requires just the schema (which is available through the Context), and the textual payload.
* All the other information are encoded in the textual payload as per the standard.
@@ -243,6 +244,7 @@ ParsedOp Context::parseOp(const std::string& input, const DataFormat format, con
auto in = wrap_ly_in_new_memory(input);
switch (opType) {
+ case OperationType::RpcYang:
case OperationType::RpcNetconf:
case OperationType::NotificationNetconf:
case OperationType::NotificationRestconf:
@@ -254,7 +256,7 @@ ParsedOp Context::parseOp(const std::string& input, const DataFormat format, con
ParsedOp res;
res.tree = tree ? std::optional{libyang::wrapRawNode(tree)} : std::nullopt;
- if (opType == OperationType::NotificationYang) {
+ if ((opType == OperationType::NotificationYang) || (opType == OperationType::RpcYang)) {
res.op = op && tree ? std::optional{DataNode(op, res.tree->m_refs)} : std::nullopt;
} else {
res.op = op ? std::optional{libyang::wrapRawNode(op)} : std::nullopt;
diff --git a/tests/context.cpp b/tests/context.cpp
index 71ae873..11019eb 100644
--- a/tests/context.cpp
+++ b/tests/context.cpp
@@ -464,6 +464,27 @@ TEST_CASE("context")
REQUIRE(data->findPath("/example-schema:leafInt8")->asTerm().valueStr() == "-43");
}
+ DOCTEST_SUBCASE("Context::parseOp")
+ {
+ DOCTEST_SUBCASE("RPC")
+ {
+ ctx->parseModule(example_schema, libyang::SchemaFormat::YANG);
+ std::string dataJson = R"({"example-schema:myRpc":{"inputLeaf":"str"}})";
+ auto pop = ctx->parseOp(dataJson, libyang::DataFormat::JSON, libyang::OperationType::RpcYang);
+ REQUIRE(pop.op->schema().name() == "myRpc");
+ REQUIRE(pop.tree->findPath("/example-schema:myRpc/inputLeaf")->asTerm().valueStr() == "str");
+ }
+ DOCTEST_SUBCASE("action")
+ {
+ ctx->parseModule(example_schema, libyang::SchemaFormat::YANG);
+ std::string datajson = R"({"example-schema:person":[{"name":"john", "poke":{}}]})";
+ auto pop = ctx->parseOp(datajson, libyang::DataFormat::JSON, libyang::OperationType::RpcYang);
+ REQUIRE(pop.op->schema().name() == "poke");
+ REQUIRE(pop.tree->findPath("/example-schema:person[name='john']/poke")->schema().nodeType() == libyang::NodeType::Action);
+ }
+ }
+
+
DOCTEST_SUBCASE("Context::parseExt")
{
ctx->setSearchDir(TESTS_DIR / "yang");
--
2.43.0
@@ -1,53 +0,0 @@
From 7e015f3486bdbb54f1dcc2e2ce51102b1d623081 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= <tomas.pecka@cesnet.cz>
Date: Wed, 23 Oct 2024 12:52:24 +0200
Subject: [PATCH 02/18] throw when lyd_validate_all returns error
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
Bug: https://github.com/CESNET/libyang-cpp/issues/20
Change-Id: I005a2f1b057978573a4046e7b4cc31d77e36fde3
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
src/DataNode.cpp | 4 +++-
tests/data_node.cpp | 7 +++++++
2 files changed, 10 insertions(+), 1 deletion(-)
diff --git a/src/DataNode.cpp b/src/DataNode.cpp
index 51c86c8..2ef17f2 100644
--- a/src/DataNode.cpp
+++ b/src/DataNode.cpp
@@ -1170,7 +1170,9 @@ void validateAll(std::optional<libyang::DataNode>& node, const std::optional<Val
}
// TODO: support the `diff` argument
- lyd_validate_all(node ? &node->m_node : nullptr, nullptr, opts ? utils::toValidationOptions(*opts) : 0, nullptr);
+ auto ret = lyd_validate_all(node ? &node->m_node : nullptr, nullptr, opts ? utils::toValidationOptions(*opts) : 0, nullptr);
+ throwIfError(ret, "libyang:validateAll: lyd_validate_all failed");
+
if (!node->m_node) {
node = std::nullopt;
}
diff --git a/tests/data_node.cpp b/tests/data_node.cpp
index a23e4c2..8a2610e 100644
--- a/tests/data_node.cpp
+++ b/tests/data_node.cpp
@@ -489,6 +489,13 @@ TEST_CASE("Data Node manipulation")
REQUIRE_THROWS_WITH_AS(libyang::validateAll(node, libyang::ValidationOptions::NoState), "validateAll: Node is not a unique reference", libyang::Error);
}
+ DOCTEST_SUBCASE("validateAll throws on validation failure")
+ {
+ ctx.parseModule(type_module, libyang::SchemaFormat::YANG);
+ auto node = std::optional{ctx.newPath("/type_module:leafWithConfigFalse", "hi")};
+ REQUIRE_THROWS_WITH_AS(libyang::validateAll(node, libyang::ValidationOptions::NoState), "libyang:validateAll: lyd_validate_all failed: LY_EVALID", libyang::ErrorWithCode);
+ }
+
DOCTEST_SUBCASE("unlink")
{
auto root = ctx.parseData(data2, libyang::DataFormat::JSON);
--
2.43.0
@@ -1,38 +0,0 @@
From 490d8bb242d33213b948485f5b94c55e22cf86a6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= <jan.kundrat@cesnet.cz>
Date: Thu, 21 Nov 2024 11:32:44 +0100
Subject: [PATCH 03/18] remove a misleading comment
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
The whole intention within action's input/output handling here was to
put some emphasis on the fact that we aren't tracking the input/output
nodes directly. However, looking at all the other classes this is a bit
redundant, we're using a pattern like this all the time. Just drop the
comment.
Change-Id: Ibd9bf9f1e83c650dda3bc43ef48e61dd6d95da5a
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
src/SchemaNode.cpp | 3 ---
1 file changed, 3 deletions(-)
diff --git a/src/SchemaNode.cpp b/src/SchemaNode.cpp
index 81e938f..9934cea 100644
--- a/src/SchemaNode.cpp
+++ b/src/SchemaNode.cpp
@@ -640,9 +640,6 @@ bool List::isUserOrdered() const
*/
ActionRpcInput ActionRpc::input() const
{
- // I need a lysc_node* for ActionRpcInput, but m_node->input is a lysp_node_action_inout. lysp_node_action_inout is
- // still just a lysc_node, so I'll just convert to lysc_node.
- // This is not very pretty, but I don't want to introduce another member for ActionRpcInput and ActionRpcOutput.
return ActionRpcInput{reinterpret_cast<const lysc_node*>(&reinterpret_cast<const lysc_node_action*>(m_node)->input), m_ctx};
}
--
2.43.0
@@ -1,35 +0,0 @@
From e1b17386cf61048d2fe27fffb3b763981a225f52 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bed=C5=99ich=20Schindler?= <bedrich.schindler@gmail.com>
Date: Wed, 27 Nov 2024 09:47:47 +0100
Subject: [PATCH 04/18] schema: improve `List::keys()` not to use `std::move`
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
`List::keys()` used `std::move` while iterating over array of leafs.
This was solved without using `std::move`.
Change-Id: I8cbf8780ecd8848e46c1de5d4123a08624536bba
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
src/SchemaNode.cpp | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/src/SchemaNode.cpp b/src/SchemaNode.cpp
index 9934cea..20e2aff 100644
--- a/src/SchemaNode.cpp
+++ b/src/SchemaNode.cpp
@@ -593,8 +593,7 @@ std::vector<Leaf> List::keys() const
LY_LIST_FOR(list->child, elem)
{
if (lysc_is_key(elem)) {
- Leaf leaf(elem, m_ctx);
- res.emplace_back(std::move(leaf));
+ res.emplace_back(Leaf(elem, m_ctx));
}
}
--
2.43.0
@@ -1,124 +0,0 @@
From 1102ecdcafbc9206f59b383769687e418557838e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bed=C5=99ich=20Schindler?= <bedrich.schindler@gmail.com>
Date: Mon, 25 Nov 2024 15:54:02 +0100
Subject: [PATCH 05/18] schema: make leaf-list's `default` statement available
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
Make leaf-list's `default` statement available so that it can be
accessed if end-user requires reading schema nodes.
`LeafList::defaultValuesStr()` returns array of canonized string default
values.
Change-Id: Idc42cd877f1fd3d717d491d09c46b59492527bff
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
include/libyang-cpp/SchemaNode.hpp | 1 +
src/SchemaNode.cpp | 17 +++++++++++++++++
tests/context.cpp | 1 +
tests/example_schema.hpp | 8 ++++++++
tests/schema_node.cpp | 7 +++++++
5 files changed, 34 insertions(+)
diff --git a/include/libyang-cpp/SchemaNode.hpp b/include/libyang-cpp/SchemaNode.hpp
index 83afc06..8ddf9be 100644
--- a/include/libyang-cpp/SchemaNode.hpp
+++ b/include/libyang-cpp/SchemaNode.hpp
@@ -169,6 +169,7 @@ class LIBYANG_CPP_EXPORT LeafList : public SchemaNode {
public:
bool isMandatory() const;
types::Type valueType() const;
+ std::vector<std::string> defaultValuesStr() const;
libyang::types::constraints::ListSize maxElements() const;
libyang::types::constraints::ListSize minElements() const;
std::optional<std::string> units() const;
diff --git a/src/SchemaNode.cpp b/src/SchemaNode.cpp
index 9934cea..95bc09b 100644
--- a/src/SchemaNode.cpp
+++ b/src/SchemaNode.cpp
@@ -472,6 +472,23 @@ types::Type LeafList::valueType() const
return types::Type{reinterpret_cast<const lysc_node_leaflist*>(m_node)->type, typeParsed, m_ctx};
}
+/**
+ * @brief Retrieves the default string values for this leaf-list.
+ *
+ * @return The default values, or an empty vector if the leaf-list does not have default values.
+ *
+ * Wraps `lysc_node_leaflist::dflts`.
+ */
+std::vector<std::string> LeafList::defaultValuesStr() const
+{
+ auto dflts = reinterpret_cast<const lysc_node_leaflist*>(m_node)->dflts;
+ std::vector<std::string> res;
+ for (const auto& it : std::span(dflts, LY_ARRAY_COUNT(dflts))) {
+ res.emplace_back(lyd_value_get_canonical(m_ctx.get(), it));
+ }
+ return res;
+}
+
/**
* @brief Retrieves the units for this leaf.
* @return The units, or std::nullopt if no units are available.
diff --git a/tests/context.cpp b/tests/context.cpp
index 11019eb..902faf6 100644
--- a/tests/context.cpp
+++ b/tests/context.cpp
@@ -733,6 +733,7 @@ TEST_CASE("context")
+--rw iid-valid? instance-identifier
+--rw iid-relaxed? instance-identifier
+--rw leafListBasic* string
+ +--rw leafListWithDefault* int32
+--rw leafListWithMinMaxElements* int32
+--rw leafListWithUnits* int32
+--rw listBasic* [primary-key]
diff --git a/tests/example_schema.hpp b/tests/example_schema.hpp
index c093f50..2861b1a 100644
--- a/tests/example_schema.hpp
+++ b/tests/example_schema.hpp
@@ -525,6 +525,14 @@ module type_module {
ordered-by user;
}
+ leaf-list leafListWithDefault {
+ type int32;
+ default -1;
+ default +512;
+ default 0x400;
+ default 04000;
+ }
+
leaf-list leafListWithMinMaxElements {
type int32;
min-elements 1;
diff --git a/tests/schema_node.cpp b/tests/schema_node.cpp
index a86900d..80c7407 100644
--- a/tests/schema_node.cpp
+++ b/tests/schema_node.cpp
@@ -200,6 +200,7 @@ TEST_CASE("SchemaNode")
"/type_module:iid-valid",
"/type_module:iid-relaxed",
"/type_module:leafListBasic",
+ "/type_module:leafListWithDefault",
"/type_module:leafListWithMinMaxElements",
"/type_module:leafListWithUnits",
"/type_module:listBasic",
@@ -606,6 +607,12 @@ TEST_CASE("SchemaNode")
REQUIRE(!ctx->findPath("/type_module:leafListBasic").asLeafList().isMandatory());
}
+ DOCTEST_SUBCASE("LeafList::defaultValuesStr")
+ {
+ REQUIRE(ctx->findPath("/type_module:leafListWithDefault").asLeafList().defaultValuesStr() == std::vector<std::string>{"-1", "512", "1024", "2048"});
+ REQUIRE(ctx->findPath("/type_module:leafListBasic").asLeafList().defaultValuesStr().size() == 0);
+ }
+
DOCTEST_SUBCASE("LeafList::maxElements")
{
REQUIRE(ctx->findPath("/type_module:leafListWithMinMaxElements").asLeafList().maxElements() == 5);
--
2.43.0
@@ -1,434 +0,0 @@
From 01f2633cef60495d5cafc4b4b1f25273b03ab3cd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bed=C5=99ich=20Schindler?= <bedrich.schindler@gmail.com>
Date: Tue, 22 Oct 2024 15:11:30 +0200
Subject: [PATCH 06/18] schema: Make choice and case statements available
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
Make choice and case statements available so that they can be accessed
if end-user requires reading schema nodes.
By design, choice and case statements do not exist in data tree directly.
Only children of one case can be present in the data tree at one time.
That means that choice and case children are not instantiable, thus
`SchemaNode::immediateChildren` must be used (instead of
`SchemaNode::childInstantibles`) if end-user wants to access choice
and case substatements.
Change-Id: Ib089672ad21dda8a0344895835d92d3432fcccb8
Co-authored-by: Jan Kundrát <jan.kundrat@cesnet.cz>
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
include/libyang-cpp/SchemaNode.hpp | 34 +++++++++++++
src/SchemaNode.cpp | 68 ++++++++++++++++++++++++++
tests/context.cpp | 77 +++++++++++++++++++-----------
tests/example_schema.hpp | 60 +++++++++++++++++++++++
tests/schema_node.cpp | 72 ++++++++++++++++++++++++++++
5 files changed, 284 insertions(+), 27 deletions(-)
diff --git a/include/libyang-cpp/SchemaNode.hpp b/include/libyang-cpp/SchemaNode.hpp
index 8ddf9be..0f1a4c4 100644
--- a/include/libyang-cpp/SchemaNode.hpp
+++ b/include/libyang-cpp/SchemaNode.hpp
@@ -22,6 +22,8 @@ class AnyDataAnyXML;
class ActionRpc;
class ActionRpcInput;
class ActionRpcOutput;
+class Case;
+class Choice;
class Container;
class Leaf;
class LeafList;
@@ -62,6 +64,8 @@ public:
// drectly by the user.
// TODO: turn these into a templated `as<>` method.
AnyDataAnyXML asAnyDataAnyXML() const;
+ Case asCase() const;
+ Choice asChoice() const;
Container asContainer() const;
Leaf asLeaf() const;
LeafList asLeafList() const;
@@ -129,6 +133,36 @@ private:
using SchemaNode::SchemaNode;
};
+/**
+ * @brief Class representing a schema definition of a `case` node.
+ *
+ * Wraps `lysc_node_case`.
+ */
+class LIBYANG_CPP_EXPORT Case : public SchemaNode {
+public:
+ friend SchemaNode;
+ friend Choice;
+
+private:
+ using SchemaNode::SchemaNode;
+};
+
+/**
+ * @brief Class representing a schema definition of a `choice` node.
+ *
+ * Wraps `lysc_node_choice`.
+ */
+class LIBYANG_CPP_EXPORT Choice : public SchemaNode {
+public:
+ bool isMandatory() const;
+ std::vector<Case> cases() const;
+ std::optional<Case> defaultCase() const;
+ friend SchemaNode;
+
+private:
+ using SchemaNode::SchemaNode;
+};
+
/**
* @brief Class representing a schema definition of a `container` node.
*/
diff --git a/src/SchemaNode.cpp b/src/SchemaNode.cpp
index bd20402..26b5099 100644
--- a/src/SchemaNode.cpp
+++ b/src/SchemaNode.cpp
@@ -191,6 +191,32 @@ NodeType SchemaNode::nodeType() const
return utils::toNodeType(m_node->nodetype);
}
+/**
+ * @brief Try to cast this SchemaNode to a Case node.
+ * @throws Error If this node is not a case.
+ */
+Case SchemaNode::asCase() const
+{
+ if (nodeType() != NodeType::Case) {
+ throw Error("Schema node is not a case: " + path());
+ }
+
+ return Case{m_node, m_ctx};
+}
+
+/**
+ * @brief Try to cast this SchemaNode to a Choice node.
+ * @throws Error If this node is not a choice.
+ */
+Choice SchemaNode::asChoice() const
+{
+ if (nodeType() != NodeType::Choice) {
+ throw Error("Schema node is not a choice: " + path());
+ }
+
+ return Choice{m_node, m_ctx};
+}
+
/**
* @brief Try to cast this SchemaNode to a Container node.
* @throws Error If this node is not a container.
@@ -401,6 +427,48 @@ bool AnyDataAnyXML::isMandatory() const
return m_node->flags & LYS_MAND_TRUE;
}
+/**
+ * @brief Checks whether this choice is mandatory.
+ *
+ * Wraps flag `LYS_MAND_TRUE`.
+ */
+bool Choice::isMandatory() const
+{
+ return m_node->flags & LYS_MAND_TRUE;
+}
+
+/**
+ * @brief Retrieves the list of cases for this choice.
+ *
+ * Wraps `lysc_node_choice::cases`.
+ */
+std::vector<Case> Choice::cases() const
+{
+ auto choice = reinterpret_cast<const lysc_node_choice*>(m_node);
+ auto cases = reinterpret_cast<lysc_node*>(choice->cases);
+ std::vector<Case> res;
+ lysc_node* elem;
+ LY_LIST_FOR(cases, elem)
+ {
+ res.emplace_back(Case(elem, m_ctx));
+ }
+ return res;
+}
+
+/**
+ * @brief Retrieves the default case for this choice.
+ *
+ * Wraps `lysc_node_choice::dflt`.
+ */
+std::optional<Case> Choice::defaultCase() const
+{
+ auto choice = reinterpret_cast<const lysc_node_choice*>(m_node);
+ if (!choice->dflt) {
+ return std::nullopt;
+ }
+ return Case{reinterpret_cast<lysc_node*>(choice->dflt), m_ctx};
+}
+
/**
* @brief Checks whether this container is mandatory.
*
diff --git a/tests/context.cpp b/tests/context.cpp
index 902faf6..5929b75 100644
--- a/tests/context.cpp
+++ b/tests/context.cpp
@@ -709,33 +709,56 @@ TEST_CASE("context")
auto mod = ctx_pp->parseModule(type_module, libyang::SchemaFormat::YANG);
REQUIRE(mod.printStr(libyang::SchemaOutputFormat::Tree) == R"(module: type_module
- +--rw anydataBasic? anydata
- +--rw anydataWithMandatoryChild anydata
- +--rw anyxmlBasic? anyxml
- +--rw anyxmlWithMandatoryChild anyxml
- +--rw leafBinary? binary
- +--rw leafBits? bits
- +--rw leafEnum? enumeration
- +--rw leafEnum2? enumeration
- +--rw leafNumber? int32
- +--rw leafRef? -> /custom-prefix:listAdvancedWithOneKey/lol
- +--rw leafRefRelaxed? -> /custom-prefix:listAdvancedWithOneKey/lol
- +--rw leafString? string
- +--rw leafUnion? union
- +--rw meal? identityref
- +--ro leafWithConfigFalse? string
- +--rw leafWithDefaultValue? string
- +--rw leafWithDescription? string
- +--rw leafWithMandatoryTrue string
- x--rw leafWithStatusDeprecated? string
- o--rw leafWithStatusObsolete? string
- +--rw leafWithUnits? int32
- +--rw iid-valid? instance-identifier
- +--rw iid-relaxed? instance-identifier
- +--rw leafListBasic* string
- +--rw leafListWithDefault* int32
- +--rw leafListWithMinMaxElements* int32
- +--rw leafListWithUnits* int32
+ +--rw anydataBasic? anydata
+ +--rw anydataWithMandatoryChild anydata
+ +--rw anyxmlBasic? anyxml
+ +--rw anyxmlWithMandatoryChild anyxml
+ +--rw choiceBasicContainer
+ | +--rw (choiceBasic)?
+ | +--:(case1)
+ | | +--rw l? string
+ | | +--rw ll* string
+ | +--:(case2)
+ | +--rw l2? string
+ +--rw choiceWithMandatoryContainer
+ | +--rw (choiceWithMandatory)
+ | +--:(case3)
+ | | +--rw l3? string
+ | +--:(case4)
+ | +--rw l4? string
+ +--rw choiceWithDefaultContainer
+ | +--rw (choiceWithDefault)?
+ | +--:(case5)
+ | | +--rw l5? string
+ | +--:(case6)
+ | +--rw l6? string
+ +--rw implicitCaseContainer
+ | +--rw (implicitCase)?
+ | +--:(implicitLeaf)
+ | +--rw implicitLeaf? string
+ +--rw leafBinary? binary
+ +--rw leafBits? bits
+ +--rw leafEnum? enumeration
+ +--rw leafEnum2? enumeration
+ +--rw leafNumber? int32
+ +--rw leafRef? -> /custom-prefix:listAdvancedWithOneKey/lol
+ +--rw leafRefRelaxed? -> /custom-prefix:listAdvancedWithOneKey/lol
+ +--rw leafString? string
+ +--rw leafUnion? union
+ +--rw meal? identityref
+ +--ro leafWithConfigFalse? string
+ +--rw leafWithDefaultValue? string
+ +--rw leafWithDescription? string
+ +--rw leafWithMandatoryTrue string
+ x--rw leafWithStatusDeprecated? string
+ o--rw leafWithStatusObsolete? string
+ +--rw leafWithUnits? int32
+ +--rw iid-valid? instance-identifier
+ +--rw iid-relaxed? instance-identifier
+ +--rw leafListBasic* string
+ +--rw leafListWithDefault* int32
+ +--rw leafListWithMinMaxElements* int32
+ +--rw leafListWithUnits* int32
+--rw listBasic* [primary-key]
| +--rw primary-key string
+--rw listAdvancedWithOneKey* [lol]
diff --git a/tests/example_schema.hpp b/tests/example_schema.hpp
index 2861b1a..ae3b4de 100644
--- a/tests/example_schema.hpp
+++ b/tests/example_schema.hpp
@@ -390,6 +390,66 @@ module type_module {
mandatory true;
}
+ container choiceBasicContainer {
+ choice choiceBasic {
+ case case1 {
+ leaf l {
+ type string;
+ }
+ leaf-list ll {
+ type string;
+ ordered-by user;
+ }
+ }
+ case case2 {
+ leaf l2 {
+ type string;
+ }
+ }
+ }
+ }
+
+ container choiceWithMandatoryContainer {
+ choice choiceWithMandatory {
+ mandatory true;
+ case case3 {
+ leaf l3 {
+ type string;
+ }
+ }
+ case case4 {
+ leaf l4 {
+ type string;
+ }
+ }
+ }
+ }
+
+ container choiceWithDefaultContainer {
+ choice choiceWithDefault {
+ default case5;
+ case case5 {
+ leaf l5 {
+ type string;
+ }
+ }
+ case case6 {
+ leaf l6 {
+ type string;
+ }
+ }
+ }
+ }
+
+ container implicitCaseContainer {
+ choice implicitCase {
+ leaf implicitLeaf {
+ type string;
+ }
+ }
+ }
+
+
leaf leafBinary {
type binary;
}
diff --git a/tests/schema_node.cpp b/tests/schema_node.cpp
index 80c7407..8d74bd2 100644
--- a/tests/schema_node.cpp
+++ b/tests/schema_node.cpp
@@ -58,6 +58,9 @@ TEST_CASE("SchemaNode")
],
"type_module:anydataWithMandatoryChild": {"content": "test-string"},
"type_module:anyxmlWithMandatoryChild": {"content": "test-string"},
+ "type_module:choiceWithMandatoryContainer": {
+ "l4": "test-string"
+ },
"type_module:containerWithMandatoryChild": {
"leafWithMandatoryTrue": "test-string"
},
@@ -180,6 +183,10 @@ TEST_CASE("SchemaNode")
"/type_module:anydataWithMandatoryChild",
"/type_module:anyxmlBasic",
"/type_module:anyxmlWithMandatoryChild",
+ "/type_module:choiceBasicContainer",
+ "/type_module:choiceWithMandatoryContainer",
+ "/type_module:choiceWithDefaultContainer",
+ "/type_module:implicitCaseContainer",
"/type_module:leafBinary",
"/type_module:leafBits",
"/type_module:leafEnum",
@@ -417,6 +424,71 @@ TEST_CASE("SchemaNode")
REQUIRE(!ctx->findPath("/type_module:anyxmlBasic").asAnyDataAnyXML().isMandatory());
}
+ DOCTEST_SUBCASE("Choice and Case")
+ {
+ std::string xpath;
+ bool isMandatory = false;
+ std::optional<std::string> defaultCase;
+ std::vector<std::string> caseNames;
+ std::optional<libyang::SchemaNode> root;
+
+ DOCTEST_SUBCASE("two cases with nothing fancy")
+ {
+ root = ctx->findPath("/type_module:choiceBasicContainer");
+ caseNames = {"case1", "case2"};
+ }
+
+ DOCTEST_SUBCASE("mandatory choice") {
+ root = ctx->findPath("/type_module:choiceWithMandatoryContainer");
+ isMandatory = true;
+ caseNames = {"case3", "case4"};
+ }
+
+ DOCTEST_SUBCASE("default choice") {
+ root = ctx->findPath("/type_module:choiceWithDefaultContainer");
+ defaultCase = "case5";
+ caseNames = {"case5", "case6"};
+ }
+
+ DOCTEST_SUBCASE("implicit case") {
+ root = ctx->findPath("/type_module:implicitCaseContainer");
+ caseNames = {"implicitLeaf"};
+ }
+
+ // For testing purposes, we have each choice in its own container. As choice and case are not directly instantiable,
+ // we wrap them in a container to simplify the testing process. It allows us to simply address the choice by its
+ // container and then get the choice from it. It also prevents polluting the test schema with unnecessary nodes
+ // and isolates the choice from other nodes.
+ auto container = root->asContainer();
+ auto choice = container.immediateChildren().begin()->asChoice();
+ REQUIRE(choice.isMandatory() == isMandatory);
+ REQUIRE(!!choice.defaultCase() == !!defaultCase);
+ if (defaultCase) {
+ REQUIRE(choice.defaultCase()->name() == *defaultCase);
+ }
+ std::vector<std::string> actualCaseNames;
+ for (const auto& case_ : choice.cases()) {
+ actualCaseNames.push_back(case_.name());
+ }
+ REQUIRE(actualCaseNames == caseNames);
+
+ // Also test child node access for one arbitrary choice/case combination
+ if (root->path() == "/type_module:choiceBasicContainer") {
+ REQUIRE(choice.cases().size() == 2);
+ auto case1 = choice.cases()[0];
+ auto children = case1.immediateChildren();
+ auto it = children.begin();
+ REQUIRE(it->asLeaf().name() == "l");
+ ++it;
+ REQUIRE(it->asLeafList().name() == "ll");
+
+ auto case2 = choice.cases()[1];
+ children = case2.immediateChildren();
+ it = children.begin();
+ REQUIRE(it->asLeaf().name() == "l2");
+ }
+ }
+
DOCTEST_SUBCASE("Container::isMandatory")
{
REQUIRE(ctx->findPath("/type_module:containerWithMandatoryChild").asContainer().isMandatory());
--
2.43.0
@@ -1,129 +0,0 @@
From a1acdc794facf8cbf113f73274ecebd5898c81a1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= <jan.kundrat@cesnet.cz>
Date: Tue, 17 Dec 2024 15:08:43 +0100
Subject: [PATCH 07/18] Wrap lyd_change_term for changing the value for a
terminal node
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
Previously, the code would require a newPath(...,
libyang::CreationOptions::Update), which is quite a mouthful.
Change-Id: I8a908c0fdd3e48dda830819758522a511adedd3b
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
include/libyang-cpp/DataNode.hpp | 8 ++++++
src/DataNode.cpp | 21 ++++++++++++++++
tests/data_node.cpp | 42 ++++++++++++++++++++++++++------
3 files changed, 63 insertions(+), 8 deletions(-)
diff --git a/include/libyang-cpp/DataNode.hpp b/include/libyang-cpp/DataNode.hpp
index 2211415..851681b 100644
--- a/include/libyang-cpp/DataNode.hpp
+++ b/include/libyang-cpp/DataNode.hpp
@@ -212,6 +212,14 @@ public:
Value value() const;
types::Type valueType() const;
+ /** @brief Was the value changed? */
+ enum class ValueChange {
+ Changed, /**< Yes, this is an actual change of the stored value */
+ ExplicitNonDefault, /**< It still holds the default value, but it's been set explicitly now */
+ EqualValueNotChanged, /**< No change, the previous value is the same as the new one, and it isn't an implicit default */
+ };
+ ValueChange changeValue(const std::string value);
+
private:
using DataNode::DataNode;
};
diff --git a/src/DataNode.cpp b/src/DataNode.cpp
index 2ef17f2..84591e5 100644
--- a/src/DataNode.cpp
+++ b/src/DataNode.cpp
@@ -903,6 +903,27 @@ types::Type DataNodeTerm::valueType() const
return impl(reinterpret_cast<const lyd_node_term*>(m_node)->value);
}
+/** @short Change the term's value
+ *
+ * Wraps `lyd_change_term`.
+ * */
+DataNodeTerm::ValueChange DataNodeTerm::changeValue(const std::string value)
+{
+ auto ret = lyd_change_term(m_node, value.c_str());
+
+ switch (ret) {
+ case LY_SUCCESS:
+ return ValueChange::Changed;
+ case LY_EEXIST:
+ return ValueChange::ExplicitNonDefault;
+ case LY_ENOT:
+ return ValueChange::EqualValueNotChanged;
+ default:
+ throwIfError(ret, "DataNodeTerm::changeValue failed");
+ __builtin_unreachable();
+ }
+}
+
/**
* @brief Returns a collection for iterating depth-first over the subtree this instance points to.
*
diff --git a/tests/data_node.cpp b/tests/data_node.cpp
index 8a2610e..45fd6c1 100644
--- a/tests/data_node.cpp
+++ b/tests/data_node.cpp
@@ -456,15 +456,41 @@ TEST_CASE("Data Node manipulation")
REQUIRE(node.hasDefaultValue());
REQUIRE(node.isImplicitDefault());
- data->newPath("/example-schema3:leafWithDefault", "not-default-value", libyang::CreationOptions::Update);
- node = data->findPath("/example-schema3:leafWithDefault")->asTerm();
- REQUIRE(!node.hasDefaultValue());
- REQUIRE(!node.isImplicitDefault());
+ DOCTEST_SUBCASE("newPath")
+ {
+ data->newPath("/example-schema3:leafWithDefault", "not-default-value", libyang::CreationOptions::Update);
+ node = data->findPath("/example-schema3:leafWithDefault")->asTerm();
+ REQUIRE(!node.hasDefaultValue());
+ REQUIRE(!node.isImplicitDefault());
- data->newPath("/example-schema3:leafWithDefault", "AHOJ", libyang::CreationOptions::Update);
- node = data->findPath("/example-schema3:leafWithDefault")->asTerm();
- REQUIRE(node.hasDefaultValue());
- REQUIRE(!node.isImplicitDefault());
+ data->newPath("/example-schema3:leafWithDefault", "AHOJ", libyang::CreationOptions::Update);
+ node = data->findPath("/example-schema3:leafWithDefault")->asTerm();
+ REQUIRE(node.hasDefaultValue());
+ REQUIRE(!node.isImplicitDefault());
+ }
+
+ DOCTEST_SUBCASE("changing values")
+ {
+ auto node = data->findPath("/example-schema3:leafWithDefault");
+ REQUIRE(!!node);
+ auto term = node->asTerm();
+
+ DOCTEST_SUBCASE("to an arbitrary value") {
+ REQUIRE(term.changeValue("cau") == libyang::DataNodeTerm::ValueChange::Changed);
+ }
+
+ DOCTEST_SUBCASE("from an implicit default to an explicit default") {
+ REQUIRE(term.changeValue("AHOJ") == libyang::DataNodeTerm::ValueChange::ExplicitNonDefault);
+ REQUIRE(term.changeValue("AHOJ") == libyang::DataNodeTerm::ValueChange::EqualValueNotChanged);
+ REQUIRE(term.changeValue("cau") == libyang::DataNodeTerm::ValueChange::Changed);
+ REQUIRE(term.changeValue("cau") == libyang::DataNodeTerm::ValueChange::EqualValueNotChanged);
+ }
+
+ DOCTEST_SUBCASE("from an implicit default to something else") {
+ REQUIRE(term.changeValue("cau") == libyang::DataNodeTerm::ValueChange::Changed);
+ REQUIRE(term.changeValue("cau") == libyang::DataNodeTerm::ValueChange::EqualValueNotChanged);
+ }
+ }
}
DOCTEST_SUBCASE("isTerm")
--
2.43.0
@@ -1,652 +0,0 @@
From 32b200ed06e9adb44a8d4ce6771f18812a54d06e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bed=C5=99ich=20Schindler?= <bedrich.schindler@gmail.com>
Date: Wed, 20 Nov 2024 10:20:19 +0100
Subject: [PATCH 08/18] Add `Module::child()`, `Module::childrenDfs()` and
`Module::immediateChildren()`
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
Those functions are implemented in the same manner as in `SchemaNode`
and allows to walk through modules children. This is counterpart to
already implemented `Module::childInstantiables()` that returns
instantiables schema nodes. These return all nodes, including
the schema-only nodes such as choice and case if end-user needs
to read its schema.
While the implementation is inspired by functions in `SchemaNode`,
imlementation of `Module::parent()` and `Module::siblings()` was omitted
as those do no make sense on `Module`.
Change-Id: I38c8374304f859d65343d04d08302e07deb05f27
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
include/libyang-cpp/Collection.hpp | 1 +
include/libyang-cpp/Module.hpp | 5 +
src/Module.cpp | 40 +++
tests/context.cpp | 5 +
tests/example_schema.hpp | 21 ++
tests/schema_node.cpp | 409 +++++++++++++++++++----------
6 files changed, 346 insertions(+), 135 deletions(-)
diff --git a/include/libyang-cpp/Collection.hpp b/include/libyang-cpp/Collection.hpp
index 557a0a2..4324791 100644
--- a/include/libyang-cpp/Collection.hpp
+++ b/include/libyang-cpp/Collection.hpp
@@ -98,6 +98,7 @@ class LIBYANG_CPP_EXPORT Collection {
public:
friend DataNode;
friend Iterator<NodeType, ITER_TYPE>;
+ friend Module;
friend SchemaNode;
~Collection();
Collection(const Collection<NodeType, ITER_TYPE>&);
diff --git a/include/libyang-cpp/Module.hpp b/include/libyang-cpp/Module.hpp
index f10c36f..ab20d36 100644
--- a/include/libyang-cpp/Module.hpp
+++ b/include/libyang-cpp/Module.hpp
@@ -34,6 +34,8 @@ class ChildInstanstiables;
class Identity;
class SchemaNode;
class SubmoduleParsed;
+template <typename NodeType, IterationType ITER_TYPE>
+class Collection;
namespace types {
class IdentityRef;
@@ -86,7 +88,10 @@ public:
std::vector<Identity> identities() const;
+ std::optional<SchemaNode> child() const;
ChildInstanstiables childInstantiables() const;
+ libyang::Collection<SchemaNode, IterationType::Dfs> childrenDfs() const;
+ Collection<SchemaNode, IterationType::Sibling> immediateChildren() const;
std::vector<SchemaNode> actionRpcs() const;
std::string printStr(const SchemaOutputFormat format, const std::optional<SchemaPrintFlags> flags = std::nullopt, std::optional<size_t> lineLength = std::nullopt) const;
diff --git a/src/Module.cpp b/src/Module.cpp
index 4dc9e3b..d6d4023 100644
--- a/src/Module.cpp
+++ b/src/Module.cpp
@@ -8,6 +8,7 @@
#include <algorithm>
#include <libyang-cpp/ChildInstantiables.hpp>
+#include <libyang-cpp/Collection.hpp>
#include <libyang-cpp/Module.hpp>
#include <libyang-cpp/Utils.hpp>
#include <libyang/libyang.h>
@@ -178,6 +179,23 @@ std::vector<Identity> Module::identities() const
return res;
}
+/**
+ * @brief Returns the first child node of this module.
+ * @return The child, or std::nullopt if there are no children.
+ */
+std::optional<SchemaNode> Module::child() const
+{
+ if (!m_module->implemented) {
+ throw Error{"Module::child: module is not implemented"};
+ }
+
+ if (!m_module->compiled->data) {
+ return std::nullopt;
+ }
+
+ return SchemaNode{m_module->compiled->data, m_ctx};
+}
+
/**
* @brief Returns a collection of data instantiable top-level nodes of this module.
*
@@ -191,6 +209,28 @@ ChildInstanstiables Module::childInstantiables() const
return ChildInstanstiables{nullptr, m_module->compiled, m_ctx};
}
+/**
+ * @brief Returns a collection for iterating depth-first over the subtree this module points to.
+ */
+Collection<SchemaNode, IterationType::Dfs> Module::childrenDfs() const
+{
+ if (!m_module->implemented) {
+ throw Error{"Module::childrenDfs: module is not implemented"};
+ }
+ return Collection<SchemaNode, IterationType::Dfs>{m_module->compiled->data, m_ctx};
+}
+
+/**
+ * @brief Returns a collection for iterating over the immediate children of where this module points to.
+ *
+ * This is a convenience function for iterating over this->child().siblings() which does not throw even when module has no children.
+ */
+Collection<SchemaNode, IterationType::Sibling> Module::immediateChildren() const
+{
+ auto c = child();
+ return c ? c->siblings() : Collection<SchemaNode, IterationType::Sibling>{nullptr, nullptr};
+}
+
/**
* @brief Returns a collection of RPC nodes (not action nodes) as SchemaNode
*
diff --git a/tests/context.cpp b/tests/context.cpp
index 5929b75..25343db 100644
--- a/tests/context.cpp
+++ b/tests/context.cpp
@@ -713,6 +713,11 @@ TEST_CASE("context")
+--rw anydataWithMandatoryChild anydata
+--rw anyxmlBasic? anyxml
+--rw anyxmlWithMandatoryChild anyxml
+ +--rw (choiceOnModule)?
+ | +--:(case1)
+ | | +--rw choiceOnModuleLeaf1? string
+ | +--:(case2)
+ | +--rw choiceOnModuleLeaf2? string
+--rw choiceBasicContainer
| +--rw (choiceBasic)?
| +--:(case1)
diff --git a/tests/example_schema.hpp b/tests/example_schema.hpp
index ae3b4de..0d8acb9 100644
--- a/tests/example_schema.hpp
+++ b/tests/example_schema.hpp
@@ -390,6 +390,19 @@ module type_module {
mandatory true;
}
+ choice choiceOnModule {
+ case case1 {
+ leaf choiceOnModuleLeaf1 {
+ type string;
+ }
+ }
+ case case2 {
+ leaf choiceOnModuleLeaf2 {
+ type string;
+ }
+ }
+ }
+
container choiceBasicContainer {
choice choiceBasic {
case case1 {
@@ -787,6 +800,14 @@ module type_module {
}
)"s;
+const auto empty_module = R"(
+module empty_module {
+ yang-version 1.1;
+ namespace "e";
+ prefix "e";
+}
+)"s;
+
const auto with_inet_types_module = R"(
module with-inet-types {
yang-version 1.1;
diff --git a/tests/schema_node.cpp b/tests/schema_node.cpp
index 8d74bd2..0001377 100644
--- a/tests/schema_node.cpp
+++ b/tests/schema_node.cpp
@@ -24,8 +24,10 @@ TEST_CASE("SchemaNode")
libyang::ContextOptions::SetPrivParsed | libyang::ContextOptions::NoYangLibrary | libyang::ContextOptions::DisableSearchCwd};
ctx->parseModule(example_schema, libyang::SchemaFormat::YANG);
ctx->parseModule(type_module, libyang::SchemaFormat::YANG);
+ ctx->parseModule(empty_module, libyang::SchemaFormat::YANG);
ctxWithParsed->parseModule(example_schema, libyang::SchemaFormat::YANG);
ctxWithParsed->parseModule(type_module, libyang::SchemaFormat::YANG);
+ ctxWithParsed->parseModule(empty_module, libyang::SchemaFormat::YANG);
DOCTEST_SUBCASE("context lifetime")
{
@@ -74,10 +76,34 @@ TEST_CASE("SchemaNode")
REQUIRE(node->schema().path() == "/example-schema:person");
}
- DOCTEST_SUBCASE("SchemaNode::child")
+ DOCTEST_SUBCASE("child")
{
- REQUIRE(ctx->findPath("/type_module:listAdvancedWithTwoKey").child()->name() == "first");
- REQUIRE(!ctx->findPath("/type_module:leafString").child().has_value());
+ DOCTEST_SUBCASE("implemented module")
+ {
+ DOCTEST_SUBCASE("SchemaNode::child")
+ {
+ REQUIRE(ctx->findPath("/type_module:listAdvancedWithTwoKey").child()->name() == "first");
+ REQUIRE(!ctx->findPath("/type_module:leafString").child().has_value());
+ }
+
+ DOCTEST_SUBCASE("Module::child")
+ {
+ REQUIRE(ctx->getModule("type_module", std::nullopt)->child()->name() == "anydataBasic");
+ REQUIRE(!ctx->getModule("empty_module", std::nullopt)->child());
+ }
+ }
+
+ DOCTEST_SUBCASE("unimplemented module")
+ {
+ DOCTEST_SUBCASE("Module::child")
+ {
+ ctx->setSearchDir(TESTS_DIR / "yang");
+ auto modYangPatch = ctx->loadModule("ietf-yang-patch", std::nullopt);
+ auto modRestconf = ctx->getModule("ietf-restconf", "2017-01-26");
+ REQUIRE(!modRestconf->implemented());
+ REQUIRE_THROWS_WITH_AS(modRestconf->child(), "Module::child: module is not implemented", libyang::Error);
+ }
+ }
}
DOCTEST_SUBCASE("SchemaNode::config")
@@ -160,162 +186,275 @@ TEST_CASE("SchemaNode")
DOCTEST_SUBCASE("childInstantiables")
{
- std::vector<std::string> expectedPaths;
- std::optional<libyang::ChildInstanstiables> children;
-
- DOCTEST_SUBCASE("SchemaNode::childInstantiables")
+ DOCTEST_SUBCASE("implemented module")
{
- expectedPaths = {
- "/type_module:listAdvancedWithOneKey/lol",
- "/type_module:listAdvancedWithOneKey/notKey1",
- "/type_module:listAdvancedWithOneKey/notKey2",
- "/type_module:listAdvancedWithOneKey/notKey3",
- "/type_module:listAdvancedWithOneKey/notKey4",
- };
+ std::vector<std::string> expectedPaths;
+ std::optional<libyang::ChildInstanstiables> children;
+
+ DOCTEST_SUBCASE("SchemaNode::childInstantiables")
+ {
+ expectedPaths = {
+ "/type_module:listAdvancedWithOneKey/lol",
+ "/type_module:listAdvancedWithOneKey/notKey1",
+ "/type_module:listAdvancedWithOneKey/notKey2",
+ "/type_module:listAdvancedWithOneKey/notKey3",
+ "/type_module:listAdvancedWithOneKey/notKey4",
+ };
+
+ children = ctx->findPath("/type_module:listAdvancedWithOneKey").childInstantiables();
+ }
- children = ctx->findPath("/type_module:listAdvancedWithOneKey").childInstantiables();
- }
+ DOCTEST_SUBCASE("Module::childInstantiables")
+ {
+ expectedPaths = {
+ "/type_module:anydataBasic",
+ "/type_module:anydataWithMandatoryChild",
+ "/type_module:anyxmlBasic",
+ "/type_module:anyxmlWithMandatoryChild",
+ "/type_module:choiceOnModuleLeaf1",
+ "/type_module:choiceOnModuleLeaf2",
+ "/type_module:choiceBasicContainer",
+ "/type_module:choiceWithMandatoryContainer",
+ "/type_module:choiceWithDefaultContainer",
+ "/type_module:implicitCaseContainer",
+ "/type_module:leafBinary",
+ "/type_module:leafBits",
+ "/type_module:leafEnum",
+ "/type_module:leafEnum2",
+ "/type_module:leafNumber",
+ "/type_module:leafRef",
+ "/type_module:leafRefRelaxed",
+ "/type_module:leafString",
+ "/type_module:leafUnion",
+ "/type_module:meal",
+ "/type_module:leafWithConfigFalse",
+ "/type_module:leafWithDefaultValue",
+ "/type_module:leafWithDescription",
+ "/type_module:leafWithMandatoryTrue",
+ "/type_module:leafWithStatusDeprecated",
+ "/type_module:leafWithStatusObsolete",
+ "/type_module:leafWithUnits",
+ "/type_module:iid-valid",
+ "/type_module:iid-relaxed",
+ "/type_module:leafListBasic",
+ "/type_module:leafListWithDefault",
+ "/type_module:leafListWithMinMaxElements",
+ "/type_module:leafListWithUnits",
+ "/type_module:listBasic",
+ "/type_module:listAdvancedWithOneKey",
+ "/type_module:listAdvancedWithTwoKey",
+ "/type_module:listWithMinMaxElements",
+ "/type_module:numeric",
+ "/type_module:container",
+ "/type_module:containerWithMandatoryChild",
+ };
+ children = ctx->getModule("type_module", std::nullopt)->childInstantiables();
+ }
- DOCTEST_SUBCASE("Module::childInstantiables")
- {
- expectedPaths = {
- "/type_module:anydataBasic",
- "/type_module:anydataWithMandatoryChild",
- "/type_module:anyxmlBasic",
- "/type_module:anyxmlWithMandatoryChild",
- "/type_module:choiceBasicContainer",
- "/type_module:choiceWithMandatoryContainer",
- "/type_module:choiceWithDefaultContainer",
- "/type_module:implicitCaseContainer",
- "/type_module:leafBinary",
- "/type_module:leafBits",
- "/type_module:leafEnum",
- "/type_module:leafEnum2",
- "/type_module:leafNumber",
- "/type_module:leafRef",
- "/type_module:leafRefRelaxed",
- "/type_module:leafString",
- "/type_module:leafUnion",
- "/type_module:meal",
- "/type_module:leafWithConfigFalse",
- "/type_module:leafWithDefaultValue",
- "/type_module:leafWithDescription",
- "/type_module:leafWithMandatoryTrue",
- "/type_module:leafWithStatusDeprecated",
- "/type_module:leafWithStatusObsolete",
- "/type_module:leafWithUnits",
- "/type_module:iid-valid",
- "/type_module:iid-relaxed",
- "/type_module:leafListBasic",
- "/type_module:leafListWithDefault",
- "/type_module:leafListWithMinMaxElements",
- "/type_module:leafListWithUnits",
- "/type_module:listBasic",
- "/type_module:listAdvancedWithOneKey",
- "/type_module:listAdvancedWithTwoKey",
- "/type_module:listWithMinMaxElements",
- "/type_module:numeric",
- "/type_module:container",
- "/type_module:containerWithMandatoryChild",
- };
- children = ctx->getModule("type_module", std::nullopt)->childInstantiables();
- }
+ std::vector<std::string> actualPaths;
+ for (const auto& child : *children) {
+ actualPaths.emplace_back(child.path());
+ }
- std::vector<std::string> actualPaths;
- for (const auto& child : *children) {
- actualPaths.emplace_back(child.path());
+ REQUIRE(expectedPaths == actualPaths);
}
- REQUIRE(expectedPaths == actualPaths);
+ DOCTEST_SUBCASE("unimplemented module")
+ {
+ DOCTEST_SUBCASE("Module::childInstantiables")
+ {
+ ctx->setSearchDir(TESTS_DIR / "yang");
+ auto modYangPatch = ctx->loadModule("ietf-yang-patch", std::nullopt);
+ auto modRestconf = ctx->getModule("ietf-restconf", "2017-01-26");
+ REQUIRE(!modRestconf->implemented());
+ REQUIRE_THROWS_WITH_AS(modRestconf->childInstantiables(), "Module::childInstantiables: module is not implemented", libyang::Error);
+ }
+ }
}
- DOCTEST_SUBCASE("SchemaNode::childrenDfs")
+ DOCTEST_SUBCASE("childrenDfs")
{
- std::vector<std::string> expectedPaths;
+ DOCTEST_SUBCASE("implemented module")
+ {
+ std::vector<std::string> expectedPaths;
+ std::optional<libyang::Collection<libyang::SchemaNode, libyang::IterationType::Dfs>> children;
- const char* path;
+ DOCTEST_SUBCASE("SchemaNode::childrenDfs")
+ {
+ DOCTEST_SUBCASE("listAdvancedWithTwoKey")
+ {
+ expectedPaths = {
+ "/type_module:listAdvancedWithTwoKey",
+ "/type_module:listAdvancedWithTwoKey/first",
+ "/type_module:listAdvancedWithTwoKey/second",
+ };
+ children = ctx->findPath("/type_module:listAdvancedWithTwoKey").childrenDfs();
+ }
- DOCTEST_SUBCASE("listAdvancedWithTwoKey")
- {
- expectedPaths = {
- "/type_module:listAdvancedWithTwoKey",
- "/type_module:listAdvancedWithTwoKey/first",
- "/type_module:listAdvancedWithTwoKey/second",
- };
+ DOCTEST_SUBCASE("DFS on a leaf")
+ {
+ expectedPaths = {
+ "/type_module:leafString",
+ };
+ children = ctx->findPath("/type_module:leafString").childrenDfs();
+ }
+ }
- path = "/type_module:listAdvancedWithTwoKey";
- }
+ DOCTEST_SUBCASE("Module::childrenDfs")
+ {
+ expectedPaths = {
+ "/type_module:anydataBasic",
+ };
+ children = ctx->getModule("type_module", std::nullopt)->childrenDfs();
+ }
- DOCTEST_SUBCASE("DFS on a leaf")
- {
- expectedPaths = {
- "/type_module:leafString",
- };
+ std::vector<std::string> actualPaths;
+ for (const auto& it : *children) {
+ actualPaths.emplace_back(it.path());
+ }
- path = "/type_module:leafString";
+ REQUIRE(actualPaths == expectedPaths);
}
- std::vector<std::string> actualPaths;
- for (const auto& it : ctx->findPath(path).childrenDfs()) {
- actualPaths.emplace_back(it.path());
+ DOCTEST_SUBCASE("unimplemented module")
+ {
+ DOCTEST_SUBCASE("Module::childrenDfs")
+ {
+ ctx->setSearchDir(TESTS_DIR / "yang");
+ auto modYangPatch = ctx->loadModule("ietf-yang-patch", std::nullopt);
+ auto modRestconf = ctx->getModule("ietf-restconf", "2017-01-26");
+ REQUIRE(!modRestconf->implemented());
+ REQUIRE_THROWS_WITH_AS(modRestconf->childrenDfs(), "Module::childrenDfs: module is not implemented", libyang::Error);
+ }
}
-
- REQUIRE(actualPaths == expectedPaths);
}
- DOCTEST_SUBCASE("SchemaNode::immediateChildren")
+ DOCTEST_SUBCASE("immediateChildren")
{
- std::vector<std::string> expectedPaths;
- const char* path;
- DOCTEST_SUBCASE("listAdvancedWithTwoKey")
- {
- expectedPaths = {
- "/type_module:listAdvancedWithTwoKey/first",
- "/type_module:listAdvancedWithTwoKey/second",
- };
- path = "/type_module:listAdvancedWithTwoKey";
- }
- DOCTEST_SUBCASE("leaf")
+ DOCTEST_SUBCASE("implemented module")
{
- expectedPaths = {
- };
- path = "/type_module:leafString";
- }
- DOCTEST_SUBCASE("no recursion")
- {
- expectedPaths = {
- "/type_module:container/x",
- "/type_module:container/y",
- "/type_module:container/z",
- };
- path = "/type_module:container";
- }
- DOCTEST_SUBCASE("empty container")
- {
- expectedPaths = {
- };
- path = "/type_module:container/y";
- }
- DOCTEST_SUBCASE("one item")
- {
- expectedPaths = {
- "/type_module:container/z/z1",
- };
- path = "/type_module:container/z";
+ std::vector<std::string> expectedPaths;
+ std::optional<libyang::Collection<libyang::SchemaNode, libyang::IterationType::Sibling>> children;
+
+ DOCTEST_SUBCASE("SchemaNode::immediateChildren")
+ {
+ DOCTEST_SUBCASE("listAdvancedWithTwoKey")
+ {
+ expectedPaths = {
+ "/type_module:listAdvancedWithTwoKey/first",
+ "/type_module:listAdvancedWithTwoKey/second",
+ };
+ children = ctx->findPath("/type_module:listAdvancedWithTwoKey").immediateChildren();
+ }
+ DOCTEST_SUBCASE("leaf")
+ {
+ expectedPaths = {};
+ children = ctx->findPath("/type_module:leafString").immediateChildren();
+ }
+ DOCTEST_SUBCASE("no recursion")
+ {
+ expectedPaths = {
+ "/type_module:container/x",
+ "/type_module:container/y",
+ "/type_module:container/z",
+ };
+ children = ctx->findPath("/type_module:container").immediateChildren();
+ }
+ DOCTEST_SUBCASE("empty container")
+ {
+ expectedPaths = {};
+ children = ctx->findPath("/type_module:container/y").immediateChildren();
+ }
+ DOCTEST_SUBCASE("one item")
+ {
+ expectedPaths = {
+ "/type_module:container/z/z1",
+ };
+ children = ctx->findPath("/type_module:container/z").immediateChildren();
+ }
+ DOCTEST_SUBCASE("two items")
+ {
+ expectedPaths = {
+ "/type_module:container/x/x1",
+ "/type_module:container/x/x2",
+ };
+ children = ctx->findPath("/type_module:container/x").immediateChildren();
+ }
+ }
+
+ DOCTEST_SUBCASE("Module::immediateChildren")
+ {
+ expectedPaths = {
+ "/type_module:anydataBasic",
+ "/type_module:anydataWithMandatoryChild",
+ "/type_module:anyxmlBasic",
+ "/type_module:anyxmlWithMandatoryChild",
+ // choiceOnModule is a choice, so it doesn't have path "/type_module:choiceOnModule".
+ // This node is tested at the end of the test subcase.
+ "/",
+ "/type_module:choiceBasicContainer",
+ "/type_module:choiceWithMandatoryContainer",
+ "/type_module:choiceWithDefaultContainer",
+ "/type_module:implicitCaseContainer",
+ "/type_module:leafBinary",
+ "/type_module:leafBits",
+ "/type_module:leafEnum",
+ "/type_module:leafEnum2",
+ "/type_module:leafNumber",
+ "/type_module:leafRef",
+ "/type_module:leafRefRelaxed",
+ "/type_module:leafString",
+ "/type_module:leafUnion",
+ "/type_module:meal",
+ "/type_module:leafWithConfigFalse",
+ "/type_module:leafWithDefaultValue",
+ "/type_module:leafWithDescription",
+ "/type_module:leafWithMandatoryTrue",
+ "/type_module:leafWithStatusDeprecated",
+ "/type_module:leafWithStatusObsolete",
+ "/type_module:leafWithUnits",
+ "/type_module:iid-valid",
+ "/type_module:iid-relaxed",
+ "/type_module:leafListBasic",
+ "/type_module:leafListWithDefault",
+ "/type_module:leafListWithMinMaxElements",
+ "/type_module:leafListWithUnits",
+ "/type_module:listBasic",
+ "/type_module:listAdvancedWithOneKey",
+ "/type_module:listAdvancedWithTwoKey",
+ "/type_module:listWithMinMaxElements",
+ "/type_module:numeric",
+ "/type_module:container",
+ "/type_module:containerWithMandatoryChild",
+ };
+ children = ctx->getModule("type_module", std::nullopt)->immediateChildren();
+
+ std::vector<std::string> actualNames;
+ for (auto it : children.value()) {
+ actualNames.emplace_back(it.name());
+ }
+ // choiceOnModule is a choice, so it doesn't have path, just name.
+ REQUIRE(actualNames[4] == "choiceOnModule");
+ }
+
+ std::vector<std::string> actualPaths;
+ for (const auto& it : *children) {
+ actualPaths.emplace_back(it.path());
+ }
+ REQUIRE(actualPaths == expectedPaths);
}
- DOCTEST_SUBCASE("two items")
+
+ DOCTEST_SUBCASE("unimplemented module")
{
- expectedPaths = {
- "/type_module:container/x/x1",
- "/type_module:container/x/x2",
- };
- path = "/type_module:container/x";
- }
- std::vector<std::string> actualPaths;
- for (const auto& it : ctx->findPath(path).immediateChildren()) {
- actualPaths.emplace_back(it.path());
+ DOCTEST_SUBCASE("Module::immediateChildren")
+ {
+ ctx->setSearchDir(TESTS_DIR / "yang");
+ auto modYangPatch = ctx->loadModule("ietf-yang-patch", std::nullopt);
+ auto modRestconf = ctx->getModule("ietf-restconf", "2017-01-26");
+ REQUIRE(!modRestconf->implemented());
+ REQUIRE_THROWS_WITH_AS(modRestconf->immediateChildren(), "Module::child: module is not implemented", libyang::Error);
+ }
}
- REQUIRE(actualPaths == expectedPaths);
}
DOCTEST_SUBCASE("SchemaNode::siblings")
--
2.43.0
@@ -1,46 +0,0 @@
From 39c7530caa510144c17521278b721ba1e6d8ff40 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= <tomas.pecka@cesnet.cz>
Date: Thu, 9 Jan 2025 15:31:37 +0100
Subject: [PATCH 09/18] upstream stopped reporting schema-mounts node
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
Change-Id: I940769d38d56fcfda3e1408c92331fdb00c161e9
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
CMakeLists.txt | 2 +-
tests/context.cpp | 3 +--
2 files changed, 2 insertions(+), 3 deletions(-)
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 3d86809..732f52b 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -28,7 +28,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)
-pkg_check_modules(LIBYANG REQUIRED libyang>=3.4.2 IMPORTED_TARGET)
+pkg_check_modules(LIBYANG REQUIRED libyang>=3.7.8 IMPORTED_TARGET)
set(LIBYANG_CPP_PKG_VERSION "3")
# FIXME from gcc 14.1 on we should be able to use the calendar/time from libstdc++ and thus remove the date dependency
diff --git a/tests/context.cpp b/tests/context.cpp
index 5929b75..9d38fea 100644
--- a/tests/context.cpp
+++ b/tests/context.cpp
@@ -509,8 +509,7 @@ TEST_CASE("context")
"error-message": "hi"
}
]
- },
- "ietf-yang-schema-mount:schema-mounts": {}
+ }
}
)");
}
--
2.43.0
@@ -1,39 +0,0 @@
From 82c963766ad4e4a802db7be656acbedb640745e9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= <jan.kundrat@cesnet.cz>
Date: Thu, 30 Jan 2025 16:34:12 +0100
Subject: [PATCH 10/18] CI: temporarily pin version due to anyxml behavior
changes upstream
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
Change-Id: Id977f4d045098c1b93656c0efb871c9a1b650e2d
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
.zuul.yaml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.zuul.yaml b/.zuul.yaml
index b41c490..19f0def 100644
--- a/.zuul.yaml
+++ b/.zuul.yaml
@@ -4,13 +4,13 @@
- f38-gcc-cover:
required-projects:
- name: github/CESNET/libyang
- override-checkout: devel
+ override-checkout: cesnet/2025-01-29
- name: github/onqtam/doctest
override-checkout: v2.3.6
- f38-clang-asan-ubsan:
required-projects: &projects
- name: github/CESNET/libyang
- override-checkout: devel
+ override-checkout: cesnet/2025-01-29
- name: github/onqtam/doctest
override-checkout: v2.4.11
- f38-clang-tsan:
--
2.43.0
@@ -1,40 +0,0 @@
From 50a216e35a555961f94a32a71bb2d45ac611d0aa Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= <jan.kundrat@cesnet.cz>
Date: Wed, 29 Jan 2025 22:49:08 +0100
Subject: [PATCH 11/18] build: a single place to define package version
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
Change-Id: I2cd7397895ed4852f852e99b97543dde76eaff8f
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
CMakeLists.txt | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 732f52b..c518ca8 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -21,7 +21,8 @@ add_custom_target(libyang-cpp-version-cmake
cmake/ProjectGitVersionRunner.cmake
)
include(cmake/ProjectGitVersion.cmake)
-prepare_git_version(LIBYANG_CPP_VERSION "3")
+set(LIBYANG_CPP_PKG_VERSION "3")
+prepare_git_version(LIBYANG_CPP_VERSION ${LIBYANG_CPP_PKG_VERSION})
find_package(Doxygen)
option(WITH_DOCS "Create and install internal documentation (needs Doxygen)" ${DOXYGEN_FOUND})
@@ -29,7 +30,6 @@ option(BUILD_SHARED_LIBS "By default, shared libs are enabled. Turn off for a st
find_package(PkgConfig REQUIRED)
pkg_check_modules(LIBYANG REQUIRED libyang>=3.7.8 IMPORTED_TARGET)
-set(LIBYANG_CPP_PKG_VERSION "3")
# 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)
--
2.43.0
@@ -1,173 +0,0 @@
From 1533458346b4f395b1187c646b61bbcb1fddc615 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= <jan.kundrat@cesnet.cz>
Date: Mon, 4 Nov 2024 14:52:12 +0100
Subject: [PATCH 12/18] YANG-flavored regular expressions
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
Change-Id: I93b2756d0f470585280c076308df3f384bd7765d
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
CMakeLists.txt | 3 +++
include/libyang-cpp/Regex.hpp | 28 ++++++++++++++++++++++
src/Regex.cpp | 45 +++++++++++++++++++++++++++++++++++
tests/regex.cpp | 30 +++++++++++++++++++++++
4 files changed, 106 insertions(+)
create mode 100644 include/libyang-cpp/Regex.hpp
create mode 100644 src/Regex.cpp
create mode 100644 tests/regex.cpp
diff --git a/CMakeLists.txt b/CMakeLists.txt
index c518ca8..512af8c 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -54,6 +54,7 @@ add_library(yang-cpp
src/Enum.cpp
src/Collection.cpp
src/Module.cpp
+ src/Regex.cpp
src/SchemaNode.cpp
src/Set.cpp
src/Type.cpp
@@ -83,6 +84,7 @@ if(${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.24")
libyang-cpp/Enum.hpp
libyang-cpp/ChildInstantiables.hpp
libyang-cpp/Module.hpp
+ libyang-cpp/Regex.hpp
libyang-cpp/Set.hpp
libyang-cpp/SchemaNode.hpp
libyang-cpp/Time.hpp
@@ -119,6 +121,7 @@ if(BUILD_TESTING)
libyang_cpp_test(schema_node)
libyang_cpp_test(unsafe)
target_link_libraries(test_unsafe PkgConfig::LIBYANG)
+ libyang_cpp_test(regex)
if(date_FOUND)
add_executable(test_time-stl-hhdate tests/time.cpp)
diff --git a/include/libyang-cpp/Regex.hpp b/include/libyang-cpp/Regex.hpp
new file mode 100644
index 0000000..31935f2
--- /dev/null
+++ b/include/libyang-cpp/Regex.hpp
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2025 CESNET, https://photonics.cesnet.cz/
+ *
+ * Written by Jan Kundrát <jan.kundrat@cesnet.cz>
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+#pragma once
+#include <libyang-cpp/export.h>
+#include <string>
+
+namespace libyang {
+class Context;
+
+/**
+ * @brief A regular expression pattern which uses the YANG-flavored regex engine
+ */
+class LIBYANG_CPP_EXPORT Regex {
+public:
+ Regex(const std::string& pattern);
+ ~Regex();
+ bool matches(const std::string& input);
+
+private:
+ void* code;
+};
+
+}
diff --git a/src/Regex.cpp b/src/Regex.cpp
new file mode 100644
index 0000000..a34fcd5
--- /dev/null
+++ b/src/Regex.cpp
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2025 CESNET, https://photonics.cesnet.cz/
+ *
+ * Written by Jan Kundrát <jan.kundrat@cesnet.cz>
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+// The following header MUST be included before anything else which "might" use PCRE2
+// because that library uses the preprocessor to prepare the "correct" versions of symbols.
+#include <libyang/tree_data.h>
+
+#include <libyang-cpp/Regex.hpp>
+#include <pcre2.h>
+#include "utils/exception.hpp"
+
+#define THE_PCRE2_CODE_P reinterpret_cast<pcre2_code*>(this->code)
+#define THE_PCRE2_CODE_P_P reinterpret_cast<pcre2_code**>(&this->code)
+
+namespace libyang {
+
+Regex::Regex(const std::string& pattern)
+ : code(nullptr)
+{
+ auto res = ly_pattern_compile(nullptr, pattern.c_str(), THE_PCRE2_CODE_P_P);
+ throwIfError(res, ly_last_logmsg());
+}
+
+Regex::~Regex()
+{
+ pcre2_code_free(THE_PCRE2_CODE_P);
+}
+
+bool Regex::matches(const std::string& input)
+{
+ auto res = ly_pattern_match(nullptr, nullptr /* we have a precompiled pattern */, input.c_str(), input.size(), THE_PCRE2_CODE_P_P);
+ if (res == LY_SUCCESS) {
+ return true;
+ } else if (res == LY_ENOT) {
+ return false;
+ } else {
+ throwError(res, ly_last_logmsg());
+ }
+}
+}
diff --git a/tests/regex.cpp b/tests/regex.cpp
new file mode 100644
index 0000000..7594f43
--- /dev/null
+++ b/tests/regex.cpp
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2025 CESNET, https://photonics.cesnet.cz/
+ *
+ * Written by Jan Kundrát <jan.kundrat@cesnet.cz>
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+#include <doctest/doctest.h>
+#include <libyang-cpp/Regex.hpp>
+#include <libyang-cpp/Utils.hpp>
+
+TEST_CASE("regex")
+{
+ using libyang::Regex;
+ using namespace std::string_literals;
+
+ REQUIRE_THROWS_WITH_AS(Regex{"\\"}, R"(Regular expression "\" is not valid ("": \ at end of pattern).: LY_EVALID)", libyang::ErrorWithCode);
+
+ Regex re{"ahoj"};
+ REQUIRE(re.matches("ahoj"));
+ REQUIRE(!re.matches("cau"));
+ REQUIRE(re.matches("ahoj")); // test repeated calls as well
+ REQUIRE(!re.matches("oj"));
+ REQUIRE(!re.matches("aho"));
+
+ // Testing runtime errors during pattern *matching* is tricky. There's a limit on backtracking,
+ // so testing a pattern like x+x+y on an obscenely long string of "x" characters *will* do the trick, eventually,
+ // but the PCRE2 library has a default limit of 10M attempts. That's a VERY big number to hit during a test :(.
+}
--
2.43.0
@@ -1,67 +0,0 @@
From 8d406728a53c2e77a4fe7393b7e30d42b8f9b9bb Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= <jan.kundrat@cesnet.cz>
Date: Thu, 30 Jan 2025 15:40:22 +0100
Subject: [PATCH 13/18] Adapt to upstream changes in anyxml JSON printing
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
Change-Id: I5f6de28cebc95a446549017c2768b450f4fd6526
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
.zuul.yaml | 4 ++--
CMakeLists.txt | 3 ++-
tests/data_node.cpp | 2 +-
3 files changed, 5 insertions(+), 4 deletions(-)
diff --git a/.zuul.yaml b/.zuul.yaml
index 19f0def..b41c490 100644
--- a/.zuul.yaml
+++ b/.zuul.yaml
@@ -4,13 +4,13 @@
- f38-gcc-cover:
required-projects:
- name: github/CESNET/libyang
- override-checkout: cesnet/2025-01-29
+ override-checkout: devel
- name: github/onqtam/doctest
override-checkout: v2.3.6
- f38-clang-asan-ubsan:
required-projects: &projects
- name: github/CESNET/libyang
- override-checkout: cesnet/2025-01-29
+ override-checkout: devel
- name: github/onqtam/doctest
override-checkout: v2.4.11
- f38-clang-tsan:
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 512af8c..a40fd52 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -29,7 +29,8 @@ 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)
-pkg_check_modules(LIBYANG REQUIRED libyang>=3.7.8 IMPORTED_TARGET)
+# 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)
# 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/tests/data_node.cpp b/tests/data_node.cpp
index 45fd6c1..14470dd 100644
--- a/tests/data_node.cpp
+++ b/tests/data_node.cpp
@@ -1568,7 +1568,7 @@ TEST_CASE("Data Node manipulation")
REQUIRE(*jsonAnyXmlNode.createdNode->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::Shrink | libyang::PrintFlags::WithSiblings)
== R"|({"example-schema:ax":[1,2,3]})|"s);
REQUIRE(*jsonAnyXmlNode.createdNode->printStr(libyang::DataFormat::XML, libyang::PrintFlags::Shrink | libyang::PrintFlags::WithSiblings)
- == R"|(<ax xmlns="http://example.com/coze"/>)|"s);
+ == R"|(<ax xmlns="http://example.com/coze">)|"s + origJSON + "</ax>");
}
REQUIRE(!!val);
--
2.43.0
@@ -1,57 +0,0 @@
From f050e7e4a17ef2e221ca000a544042c33c9541fc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= <jan.kundrat@cesnet.cz>
Date: Thu, 13 Mar 2025 19:21:26 +0100
Subject: [PATCH 14/18] fix DataNode::insertSibling() return value
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
When such an insert happens, the C library returns the node which is now
the first sibling among all of the siblings of the node which was used
as a reference during the insert. Our API was also documented this way,
but we were not doing that.
Reported-by: Irfan <irfan.haslanded@gmail.com>
Bug: https://github.com/CESNET/libyang-cpp/issues/29
Change-Id: Id7f84a31e50212d6e2cbce9ab03a351a3721f767
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
src/DataNode.cpp | 2 +-
tests/data_node.cpp | 7 +++++++
2 files changed, 8 insertions(+), 1 deletion(-)
diff --git a/src/DataNode.cpp b/src/DataNode.cpp
index 84591e5..b899b18 100644
--- a/src/DataNode.cpp
+++ b/src/DataNode.cpp
@@ -711,7 +711,7 @@ DataNode DataNode::insertSibling(DataNode toInsert)
lyd_insert_sibling(this->m_node, toInsert.m_node, &firstSibling);
}, toInsert.parent() ? OperationScope::JustThisNode : OperationScope::AffectsFollowingSiblings, m_refs);
- return DataNode{m_node, m_refs};
+ return DataNode{firstSibling, m_refs};
}
/**
diff --git a/tests/data_node.cpp b/tests/data_node.cpp
index 14470dd..b6ee455 100644
--- a/tests/data_node.cpp
+++ b/tests/data_node.cpp
@@ -973,6 +973,13 @@ TEST_CASE("Data Node manipulation")
REQUIRE(getNumberOrder() == expected);
}
+ DOCTEST_SUBCASE("DataNode::insertSibling")
+ {
+ auto node = ctx.newPath("/example-schema:leafUInt8", "10");
+ REQUIRE(node.insertSibling(ctx.newPath("/example-schema:leafUInt16", "10")).path() == "/example-schema:leafUInt8");
+ REQUIRE(node.insertSibling(ctx.newPath("/example-schema:dummy", "10")).path() == "/example-schema:dummy");
+ }
+
DOCTEST_SUBCASE("DataNode::duplicate")
{
auto root = ctx.parseData(data2, libyang::DataFormat::JSON);
--
2.43.0
@@ -1,260 +0,0 @@
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
@@ -1,462 +0,0 @@
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
@@ -1,56 +0,0 @@
From dab8c9aac96063ccb8541d5d1795ee89b75faeee Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= <jan.kundrat@cesnet.cz>
Date: Wed, 2 Apr 2025 11:50:24 -0700
Subject: [PATCH 17/18] build: link to libpcre2 explicitly
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
This is apparently a problem on Mac OS X builds where the transitive
dependency via libyang doesn't take effect:
Undefined symbols for architecture arm64:
"_pcre2_code_free_8", referenced from:
libyang::Regex::~Regex() in Regex.cpp.o
libyang::Regex::~Regex() in Regex.cpp.o
ld: symbol(s) not found for architecture arm64
Let's fix this by linking with libpcre2-8 directly.
We're using a different approach than libyang; they have their own CMake
wrapper. I find it easier to work with pkg-config modules, so let's try
it that way.
Change-Id: I1a64407d852161595b7dca654433190002cc3600
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
CMakeLists.txt | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/CMakeLists.txt b/CMakeLists.txt
index c5fec45..4e009aa 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -34,6 +34,9 @@ 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)
+# libyang::Regex::~Regex() bypasses libyang and calls out to libpcre directly
+pkg_check_modules(LIBPCRE2 REQUIRED libpcre2-8 IMPORTED_TARGET)
+
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/include)
include(CheckIncludeFileCXX)
@@ -64,7 +67,7 @@ add_library(yang-cpp
src/utils/newPath.cpp
)
-target_link_libraries(yang-cpp PRIVATE PkgConfig::LIBYANG)
+target_link_libraries(yang-cpp PRIVATE PkgConfig::LIBYANG PkgConfig::LIBPCRE2)
# We do not offer any long-term API/ABI guarantees. To make stuff easier for downstream consumers,
# we will be bumping both API and ABI versions very deliberately.
# There will be no attempts at semver tracking, for example.
--
2.43.0
@@ -1,66 +0,0 @@
From 7519fcd35216072a6b1eebe2a79e19789be345a9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= <jan.kundrat@cesnet.cz>
Date: Wed, 9 Apr 2025 15:35:37 +0200
Subject: [PATCH 18/18] CI: renamed project upstream
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
Change-Id: I5447f243297fbfde7c364eb3919b00db239bd069
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
.zuul.yaml | 4 ++--
README.md | 2 +-
ci/build.sh | 2 +-
3 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/.zuul.yaml b/.zuul.yaml
index b41c490..7b92766 100644
--- a/.zuul.yaml
+++ b/.zuul.yaml
@@ -5,13 +5,13 @@
required-projects:
- name: github/CESNET/libyang
override-checkout: devel
- - name: github/onqtam/doctest
+ - name: github/doctest/doctest
override-checkout: v2.3.6
- f38-clang-asan-ubsan:
required-projects: &projects
- name: github/CESNET/libyang
override-checkout: devel
- - name: github/onqtam/doctest
+ - name: github/doctest/doctest
override-checkout: v2.4.11
- f38-clang-tsan:
required-projects: *projects
diff --git a/README.md b/README.md
index d76975b..8291e00 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,7 @@ Object lifetimes are managed automatically via RAII.
- [libyang v3](https://github.com/CESNET/libyang) - the `devel` branch (even for the `master` branch of *libyang-cpp*)
- C++20 compiler (e.g., GCC 10.x+, clang 10+)
- CMake 3.19+
-- optionally for built-in tests, [Doctest](https://github.com/onqtam/doctest/) as a C++ unit test framework
+- optionally for built-in tests, [Doctest](https://github.com/doctest/doctest/) as a C++ unit test framework
- optionally for the docs, Doxygen
## Building
diff --git a/ci/build.sh b/ci/build.sh
index d06b646..0867f07 100755
--- a/ci/build.sh
+++ b/ci/build.sh
@@ -73,7 +73,7 @@ build_n_test() {
}
build_n_test github/CESNET/libyang -DENABLE_BUILD_TESTS=ON -DENABLE_VALGRIND_TESTS=OFF
-build_n_test github/onqtam/doctest -DDOCTEST_WITH_TESTS=OFF
+build_n_test github/doctest/doctest -DDOCTEST_WITH_TESTS=OFF
build_n_test ${ZUUL_PROJECT_NAME} -DBUILD_TESTING=ON
pushd ${BUILD_DIR}/${ZUUL_PROJECT_NAME}
--
2.43.0
+1 -1
View File
@@ -1,3 +1,3 @@
# Locally calculated
sha256 82e3758011ec44c78e98d0777799d6e12aec5b8a64b32ebb20d0fe50e32488bb LICENSE
sha256 43c84317ba13470f421a90bca74c062cf63a70e488d2e90c432b662c4638a14e libyang-cpp-v3.tar.gz
sha256 70fd0df940026fb930d5407abe679e064caf4701bff35d79465b9ad2c0915808 libyang-cpp-v4.tar.gz
+1 -1
View File
@@ -3,7 +3,7 @@
# CPP bindings for libyang
#
################################################################################
LIBYANG_CPP_VERSION = v3
LIBYANG_CPP_VERSION = v4
LIBYANG_CPP_SITE = $(call github,CESNET,libyang-cpp,$(LIBYANG_CPP_VERSION))
LIBYANG_CPP_LICENSE = BSD-3-Clause
LIBYANG_CPP_LICENSE_FILES = LICENSE