From 0a09b00dab204bb5890978a17bb23c4c25280580 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= Date: Sun, 24 Mar 2024 20:41:25 +0100 Subject: [PATCH 01/57] Port to libyang v3 Four parts, really: - some enum reshuffling when creating new nodes - a more capable XPath finding, but we have not been exporting any "non-JSON XPaths", whatever that might be - system-ordered lists are now ordered alphabetically - a more capable error handling API Change-Id: Ic6de06f004c16e56cef93fae0136c07d903394c0 --- .zuul.yaml | 4 ++-- CMakeLists.txt | 2 +- README.md | 3 +-- include/libyang-cpp/Context.hpp | 4 +++- include/libyang-cpp/Enum.hpp | 14 +++++++++----- src/Context.cpp | 6 ++++-- src/DataNode.cpp | 3 ++- src/utils/enum.hpp | 9 ++++++--- tests/context.cpp | 16 ++++++++++++---- tests/data_node.cpp | 29 +++++++++++++++-------------- tests/pretty_printers.hpp | 4 +++- 11 files changed, 58 insertions(+), 36 deletions(-) diff --git a/.zuul.yaml b/.zuul.yaml index d9d95e16..b41c4904 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -4,13 +4,13 @@ - f38-gcc-cover: required-projects: - name: github/CESNET/libyang - override-checkout: cesnet/2024-03--v2-latest + 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/2024-03--v2-latest + override-checkout: devel - name: github/onqtam/doctest override-checkout: v2.4.11 - f38-clang-tsan: diff --git a/CMakeLists.txt b/CMakeLists.txt index 288b62a9..6e547075 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>=2.1.140 IMPORTED_TARGET) +pkg_check_modules(LIBYANG REQUIRED libyang>=2.2.3 IMPORTED_TARGET) set(LIBYANG_CPP_PKG_VERSION "1.1.0") include_directories(${CMAKE_CURRENT_SOURCE_DIR}/include) diff --git a/README.md b/README.md index 5e79a23e..9009a540 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,7 @@ Object lifetimes are managed automatically via RAII. ## Dependencies -- [libyang](https://github.com/CESNET/libyang) - ~~the `devel` branch (even for the `master` branch of *libyang-cpp*)~~ - - temporarily (March 2024) this requires the [pre-v3 API of libyang (commit `b3d079fc3`)](https://github.com/CESNET/libyang/tree/b3d079fc37ac119f5a2a4a120b86d1c7102c3a08) +- [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+ diff --git a/include/libyang-cpp/Context.hpp b/include/libyang-cpp/Context.hpp index 1dab9f31..6f6e2330 100644 --- a/include/libyang-cpp/Context.hpp +++ b/include/libyang-cpp/Context.hpp @@ -69,7 +69,9 @@ struct LIBYANG_CPP_EXPORT ErrorInfo { LogLevel level; std::string message; ErrorCode code; - std::optional path; + std::optional dataPath; + std::optional schemaPath; + uint64_t line; ValidationErrorCode validationCode; }; diff --git a/include/libyang-cpp/Enum.hpp b/include/libyang-cpp/Enum.hpp index 3ded4d2c..85dcc838 100644 --- a/include/libyang-cpp/Enum.hpp +++ b/include/libyang-cpp/Enum.hpp @@ -115,11 +115,15 @@ enum class ValidationErrorCode : uint32_t { }; enum class CreationOptions : uint32_t { - Update = 0x01, - Output = 0x02, - Opaque = 0x04, - // BinaryLyb = 0x08, TODO - CanonicalValue = 0x10 + Output = 0x01, + StoreOnly = 0x02, + // BinaryLyb = 0x04, TODO + CanonicalValue = 0x08, + ClearDefaultFromParents = 0x10, + Update = 0x20, + Opaque = 0x40, + PathWithOpaque = 0x80, + // LYD_NEW_ANY_USE_VALUE is not relevant }; /** diff --git a/src/Context.cpp b/src/Context.cpp index 337070f3..713c50fb 100644 --- a/src/Context.cpp +++ b/src/Context.cpp @@ -579,8 +579,10 @@ std::vector Context::getErrors() const .appTag = errIt->apptag ? std::optional{errIt->apptag} : std::nullopt, .level = utils::toLogLevel(errIt->level), .message = errIt->msg, - .code = static_cast(errIt->no), - .path = errIt->path ? std::optional{errIt->path} : std::nullopt, + .code = static_cast(errIt->err), + .dataPath = errIt->data_path ? std::optional{errIt->data_path} : std::nullopt, + .schemaPath = errIt->schema_path ? std::optional{errIt->schema_path} : std::nullopt, + .line = errIt->line, .validationCode = utils::toValidationErrorCode(errIt->vecode) }); diff --git a/src/DataNode.cpp b/src/DataNode.cpp index ab8cb4a2..0e609c40 100644 --- a/src/DataNode.cpp +++ b/src/DataNode.cpp @@ -1187,7 +1187,8 @@ Set findXPathAt( const std::string& xpath) { ly_set* set; - auto ret = lyd_find_xpath3(contextNode ? contextNode->m_node : nullptr, forest.m_node, xpath.c_str(), nullptr, &set); + auto ret = lyd_find_xpath3(contextNode ? contextNode->m_node : nullptr, forest.m_node, xpath.c_str(), + LY_VALUE_JSON, nullptr, nullptr, &set); throwIfError(ret, "libyang::findXPathAt:"); diff --git a/src/utils/enum.hpp b/src/utils/enum.hpp index 7674fc6c..5e02d241 100644 --- a/src/utils/enum.hpp +++ b/src/utils/enum.hpp @@ -72,11 +72,14 @@ constexpr uint32_t toCreationOptions(const CreationOptions flags) { return static_cast(flags); } +static_assert(LYD_NEW_VAL_OUTPUT == toCreationOptions(CreationOptions::Output)); +static_assert(LYD_NEW_VAL_STORE_ONLY == toCreationOptions(CreationOptions::StoreOnly)); +// static_assert(LYD_NEW_PATH_BIN_VALUE == toCreationOptions(CreationOptions::BinaryLyb)); +static_assert(LYD_NEW_VAL_CANON == toCreationOptions(CreationOptions::CanonicalValue)); +static_assert(LYD_NEW_META_CLEAR_DFLT == toCreationOptions(CreationOptions::ClearDefaultFromParents)); static_assert(LYD_NEW_PATH_UPDATE == toCreationOptions(CreationOptions::Update)); -static_assert(LYD_NEW_PATH_OUTPUT == toCreationOptions(CreationOptions::Output)); static_assert(LYD_NEW_PATH_OPAQ == toCreationOptions(CreationOptions::Opaque)); -// static_assert(LYD_NEW_PATH_BIN_VALUE == toCreationOptions(CreationOptions::BinaryLyb)); -static_assert(LYD_NEW_PATH_CANON_VALUE == toCreationOptions(CreationOptions::CanonicalValue)); +static_assert(LYD_NEW_PATH_WITH_OPAQ == toCreationOptions(CreationOptions::PathWithOpaque)); constexpr uint32_t toDuplicationOptions(const DuplicationOptions options) { diff --git a/tests/context.cpp b/tests/context.cpp index d19f78b9..cb629ef1 100644 --- a/tests/context.cpp +++ b/tests/context.cpp @@ -457,7 +457,9 @@ TEST_CASE("context") .level = libyang::LogLevel::Error, .message = "Invalid character sequence \"invalid\", expected a keyword.", .code = libyang::ErrorCode::ValidationFailure, - .path = "Line number 1.", + .dataPath = std::nullopt, + .schemaPath = std::nullopt, + .line = 1, .validationCode = libyang::ValidationErrorCode::Syntax, } }; @@ -476,7 +478,9 @@ TEST_CASE("context") .level = libyang::LogLevel::Error, .message = "Value \"9001\" is out of type int8 min/max bounds.", .code = libyang::ErrorCode::ValidationFailure, - .path = "Schema location \"/example-schema:leafInt8\".", + .dataPath = std::nullopt, + .schemaPath = "/example-schema:leafInt8", + .line = 0, .validationCode = libyang::ValidationErrorCode::Data, } }; @@ -491,7 +495,9 @@ TEST_CASE("context") .level = libyang::LogLevel::Error, .message = "Invalid type int8 empty value.", .code = libyang::ErrorCode::ValidationFailure, - .path = "Schema location \"/example-schema:leafInt8\".", + .dataPath = std::nullopt, + .schemaPath = "/example-schema:leafInt8", + .line = 0, .validationCode = libyang::ValidationErrorCode::Data, }, libyang::ErrorInfo { @@ -499,7 +505,9 @@ TEST_CASE("context") .level = libyang::LogLevel::Error, .message = "Value \"9001\" is out of type int8 min/max bounds.", .code = libyang::ErrorCode::ValidationFailure, - .path = "Schema location \"/example-schema:leafInt8\".", + .dataPath = std::nullopt, + .schemaPath = "/example-schema:leafInt8", + .line = 0, .validationCode = libyang::ValidationErrorCode::Data, } }; diff --git a/tests/data_node.cpp b/tests/data_node.cpp index 15fb727a..2d279794 100644 --- a/tests/data_node.cpp +++ b/tests/data_node.cpp @@ -1026,12 +1026,12 @@ TEST_CASE("Data Node manipulation") "/example-schema:bigTree/one", "/example-schema:bigTree/one/myLeaf", "/example-schema:bigTree/two", - "/example-schema:bigTree/two/myList[thekey='43221']", - "/example-schema:bigTree/two/myList[thekey='43221']/thekey", + "/example-schema:bigTree/two/myList[thekey='213']", + "/example-schema:bigTree/two/myList[thekey='213']/thekey", "/example-schema:bigTree/two/myList[thekey='432']", "/example-schema:bigTree/two/myList[thekey='432']/thekey", - "/example-schema:bigTree/two/myList[thekey='213']", - "/example-schema:bigTree/two/myList[thekey='213']/thekey" + "/example-schema:bigTree/two/myList[thekey='43221']", + "/example-schema:bigTree/two/myList[thekey='43221']/thekey", }; REQUIRE(res == expected); @@ -1255,6 +1255,7 @@ TEST_CASE("Data Node manipulation") DOCTEST_SUBCASE("DataNode::findXPath") { + // libyang v3 sorts these alphabetically const auto data3 = R"({ "example-schema:person": [ { @@ -1308,13 +1309,13 @@ TEST_CASE("Data Node manipulation") auto set = node->findXPath("/example-schema:person"); REQUIRE(set.size() == 3); - REQUIRE(set.front().path() == "/example-schema:person[name='John']"); - REQUIRE(set.back().path() == "/example-schema:person[name='David']"); + REQUIRE(set.front().path() == "/example-schema:person[name='Dan']"); + REQUIRE(set.back().path() == "/example-schema:person[name='John']"); auto iter = set.begin(); - REQUIRE((iter++)->path() == "/example-schema:person[name='John']"); REQUIRE((iter++)->path() == "/example-schema:person[name='Dan']"); REQUIRE((iter++)->path() == "/example-schema:person[name='David']"); + REQUIRE((iter++)->path() == "/example-schema:person[name='John']"); REQUIRE(iter == set.end()); REQUIRE_THROWS_WITH_AS(*iter, "Dereferenced an .end() iterator", std::out_of_range); } @@ -1324,23 +1325,23 @@ TEST_CASE("Data Node manipulation") auto set = node->findXPath("/example-schema:person"); REQUIRE((set.begin() + 0) == set.begin()); - REQUIRE((set.begin() + 0)->path() == "/example-schema:person[name='John']"); - REQUIRE((set.begin() + 1)->path() == "/example-schema:person[name='Dan']"); - REQUIRE((set.begin() + 2)->path() == "/example-schema:person[name='David']"); + REQUIRE((set.begin() + 0)->path() == "/example-schema:person[name='Dan']"); + REQUIRE((set.begin() + 1)->path() == "/example-schema:person[name='David']"); + REQUIRE((set.begin() + 2)->path() == "/example-schema:person[name='John']"); REQUIRE((set.begin() + 3) == set.end()); REQUIRE_THROWS(set.begin() + 4); REQUIRE((set.end() - 0) == set.end()); - REQUIRE((set.end() - 1)->path() == "/example-schema:person[name='David']"); - REQUIRE((set.end() - 2)->path() == "/example-schema:person[name='Dan']"); - REQUIRE((set.end() - 3)->path() == "/example-schema:person[name='John']"); + REQUIRE((set.end() - 1)->path() == "/example-schema:person[name='John']"); + REQUIRE((set.end() - 2)->path() == "/example-schema:person[name='David']"); + REQUIRE((set.end() - 3)->path() == "/example-schema:person[name='Dan']"); REQUIRE((set.end() - 3) == set.begin()); REQUIRE_THROWS(set.end() - 4); auto iter = set.end(); + REQUIRE((--iter)->path() == "/example-schema:person[name='John']"); REQUIRE((--iter)->path() == "/example-schema:person[name='David']"); REQUIRE((--iter)->path() == "/example-schema:person[name='Dan']"); - REQUIRE((--iter)->path() == "/example-schema:person[name='John']"); REQUIRE_THROWS(--iter); } diff --git a/tests/pretty_printers.hpp b/tests/pretty_printers.hpp index 6ae89346..6bbc16fb 100644 --- a/tests/pretty_printers.hpp +++ b/tests/pretty_printers.hpp @@ -59,7 +59,9 @@ doctest::String toString(const std::vector& errors) oss << "appTag: " << (err.appTag ? *err.appTag : "std::nullopt") << "\n "; oss << "code: " << err.code << "\n "; oss << "message: " << err.message << "\n "; - oss << "path: " << (err.path ? *err.path : "std::nullopt") << "\n "; + oss << "dataPath: " << (err.dataPath ? *err.dataPath : "std::nullopt") << "\n "; + oss << "schemaPath: " << (err.schemaPath ? *err.schemaPath : "std::nullopt") << "\n "; + oss << "line: " << err.line << "\n "; oss << "level: " << err.level << "\n "; oss << "validationCode: " << err.validationCode << "\n }"; return oss.str(); From cc604edb5764ff748a3b469a979db1f38f77846b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= Date: Sun, 24 Mar 2024 20:07:00 +0000 Subject: [PATCH 02/57] refactor: stop returning a string_view ...because they are painful to use, IMHO. Change-Id: Icebada56cffb9f7f4393260adfe1b54b9f37b51e --- include/libyang-cpp/DataNode.hpp | 8 ++++---- include/libyang-cpp/Module.hpp | 15 +++++++------- include/libyang-cpp/SchemaNode.hpp | 14 ++++++------- include/libyang-cpp/Type.hpp | 7 +++---- src/DataNode.cpp | 18 +++++++---------- src/Module.cpp | 26 ++++++++++++------------ src/SchemaNode.cpp | 32 +++++++++++++++--------------- src/Type.cpp | 6 +++--- src/Utils.cpp | 2 +- tests/data_node.cpp | 4 ++-- tests/pretty_printers.hpp | 4 ++-- 11 files changed, 65 insertions(+), 71 deletions(-) diff --git a/include/libyang-cpp/DataNode.hpp b/include/libyang-cpp/DataNode.hpp index d7972842..8f9d087f 100644 --- a/include/libyang-cpp/DataNode.hpp +++ b/include/libyang-cpp/DataNode.hpp @@ -201,7 +201,7 @@ class LIBYANG_CPP_EXPORT Meta { */ class LIBYANG_CPP_EXPORT DataNodeTerm : public DataNode { public: - std::string_view valueStr() const; + std::string valueStr() const; bool hasDefaultValue() const; bool isImplicitDefault() const; @@ -222,8 +222,8 @@ class LIBYANG_CPP_EXPORT DataNodeTerm : public DataNode { * Wraps `ly_opaq_name`. */ struct LIBYANG_CPP_EXPORT OpaqueName { - std::optional prefix; - std::string_view name; + std::optional prefix; + std::string name; }; /** @@ -234,7 +234,7 @@ struct LIBYANG_CPP_EXPORT OpaqueName { class LIBYANG_CPP_EXPORT DataNodeOpaque : public DataNode { public: OpaqueName name() const; - std::string_view value() const; + std::string value() const; friend DataNode; private: diff --git a/include/libyang-cpp/Module.hpp b/include/libyang-cpp/Module.hpp index 8bc5c0a0..1085d48d 100644 --- a/include/libyang-cpp/Module.hpp +++ b/include/libyang-cpp/Module.hpp @@ -9,7 +9,6 @@ #include #include -#include #include #include #include @@ -46,7 +45,7 @@ class IdentityRef; */ class LIBYANG_CPP_EXPORT Feature { public: - std::string_view name() const; + std::string name() const; bool isEnabled() const; friend Module; @@ -72,9 +71,9 @@ struct LIBYANG_CPP_EXPORT AllFeatures { */ class LIBYANG_CPP_EXPORT Module { public: - std::string_view name() const; - std::optional revision() const; - std::string_view ns() const; + std::string name() const; + std::optional revision() const; + std::string ns() const; bool implemented() const; bool featureEnabled(const std::string& featureName) const; std::vector features() const; @@ -139,7 +138,7 @@ class LIBYANG_CPP_EXPORT Identity { std::vector derived() const; std::vector derivedRecursive() const; Module module() const; - std::string_view name() const; + std::string name() const; bool operator==(const Identity& other) const; @@ -158,7 +157,7 @@ class LIBYANG_CPP_EXPORT Identity { class LIBYANG_CPP_EXPORT ExtensionInstance { public: Extension definition() const; - std::string_view argument() const; + std::string argument() const; private: ExtensionInstance(const lysc_ext_instance* ext, std::shared_ptr ctx); @@ -178,7 +177,7 @@ class LIBYANG_CPP_EXPORT ExtensionInstance { */ class LIBYANG_CPP_EXPORT Extension { public: - std::string_view name() const; + std::string name() const; private: Extension(const lysc_ext* def, std::shared_ptr ctx); diff --git a/include/libyang-cpp/SchemaNode.hpp b/include/libyang-cpp/SchemaNode.hpp index f076ef6d..bb447ccb 100644 --- a/include/libyang-cpp/SchemaNode.hpp +++ b/include/libyang-cpp/SchemaNode.hpp @@ -51,8 +51,8 @@ class LIBYANG_CPP_EXPORT SchemaNode { public: Module module() const; std::string path() const; - std::string_view name() const; - std::optional description() const; + std::string name() const; + std::optional description() const; Status status() const; Config config() const; bool isInput() const; @@ -105,8 +105,8 @@ class LIBYANG_CPP_EXPORT SchemaNode { */ class LIBYANG_CPP_EXPORT When { public: - std::string_view condition() const; - std::optional description() const; + std::string condition() const; + std::optional description() const; private: const lysc_when* m_when; @@ -150,8 +150,8 @@ class LIBYANG_CPP_EXPORT Leaf : public SchemaNode { bool isKey() const; bool isMandatory() const; types::Type valueType() const; - std::optional defaultValueStr() const; - std::optional units() const; + std::optional defaultValueStr() const; + std::optional units() const; friend SchemaNode; private: @@ -169,7 +169,7 @@ class LIBYANG_CPP_EXPORT LeafList : public SchemaNode { types::Type valueType() const; libyang::types::constraints::ListSize maxElements() const; libyang::types::constraints::ListSize minElements() const; - std::optional units() const; + std::optional units() const; bool isUserOrdered() const; friend SchemaNode; diff --git a/include/libyang-cpp/Type.hpp b/include/libyang-cpp/Type.hpp index 12c5de70..faa1b2dc 100644 --- a/include/libyang-cpp/Type.hpp +++ b/include/libyang-cpp/Type.hpp @@ -13,7 +13,6 @@ #include #include #include -#include #include #include @@ -66,8 +65,8 @@ class LIBYANG_CPP_EXPORT Type { Numeric asNumeric() const; InstanceIdentifier asInstanceIdentifier() const; - std::string_view name() const; - std::optional description() const; + std::string name() const; + std::optional description() const; std::string internalPluginId() const; @@ -160,7 +159,7 @@ class LIBYANG_CPP_EXPORT LeafRef : public Type { public: friend Type; - std::string_view path() const; + std::string path() const; Type resolvedType() const; bool requireInstance() const; diff --git a/src/DataNode.cpp b/src/DataNode.cpp index 0e609c40..b24a4ed6 100644 --- a/src/DataNode.cpp +++ b/src/DataNode.cpp @@ -450,7 +450,7 @@ AnydataValue DataNodeAny::releaseValue() return XML{any->value.xml}; default: - throw std::logic_error{std::string{"Unsupported anydata value type: "} + std::to_string(any->value_type)}; + throw std::logic_error{"Unsupported anydata value type: " + std::to_string(any->value_type)}; } __builtin_unreachable(); @@ -755,11 +755,9 @@ void DataNode::merge(DataNode toMerge) } /** - * @brief Gets the value of this term node as a string_view. - * - * The string_view must not outlive the DataNodeTerm's lifetime. + * @brief Gets the value of this term node as a string. */ -std::string_view DataNodeTerm::valueStr() const +std::string DataNodeTerm::valueStr() const { return lyd_get_value(m_node); } @@ -836,8 +834,7 @@ Value DataNodeTerm::value() const return res; } case LY_TYPE_STRING: - // valueStr gives a string_view, so here I have to copy the string. - return std::string(valueStr()); + return valueStr(); case LY_TYPE_UNION: return impl(value.subvalue->value); case LY_TYPE_DEC64: { @@ -967,7 +964,7 @@ void DataNode::newMeta(const Module& module, const std::string& name, const std: // TODO: allow returning the lyd_meta struct auto ret = lyd_new_meta(m_refs->context.get(), m_node, module.m_module, name.c_str(), value.c_str(), false, nullptr); - throwIfError(ret, "DataNode::newMeta: couldn't add metadata for " + std::string{path()}); + throwIfError(ret, "DataNode::newMeta: couldn't add metadata for " + path()); } /** @@ -1092,10 +1089,9 @@ OpaqueName DataNodeOpaque::name() const }; } -std::string_view DataNodeOpaque::value() const +std::string DataNodeOpaque::value() const { - auto opaq = reinterpret_cast(m_node); - return opaq->value; + return reinterpret_cast(m_node)->value; } /** diff --git a/src/Module.cpp b/src/Module.cpp index 0e0a83e6..6df4f083 100644 --- a/src/Module.cpp +++ b/src/Module.cpp @@ -45,7 +45,7 @@ Module::Module(lys_module* module, std::shared_ptr ctx) * * Wraps `lys_module::name`. */ -std::string_view Module::name() const +std::string Module::name() const { return m_module->name; } @@ -55,7 +55,7 @@ std::string_view Module::name() const * * Wraps `lys_module::revision`. */ -std::optional Module::revision() const +std::optional Module::revision() const { if (!m_module->revision) { return std::nullopt; @@ -69,7 +69,7 @@ std::optional Module::revision() const * * Wraps `lys_module::ns`. */ -std::string_view Module::ns() const +std::string Module::ns() const { return m_module->ns; } @@ -99,7 +99,7 @@ bool Module::featureEnabled(const std::string& featureName) const case LY_ENOT: return false; case LY_ENOTFOUND: - throwError(ret, "Feature '"s + featureName + "' doesn't exist within module '" + std::string(name()) + "'"); + throwError(ret, "Feature '"s + featureName + "' doesn't exist within module '" + name() + "'"); default: throwError(ret, "Error while enabling feature"); } @@ -114,7 +114,7 @@ bool Module::featureEnabled(const std::string& featureName) const void Module::setImplemented() { auto err = lys_set_implemented(m_module, nullptr); - throwIfError(err, "Couldn't set module '" + std::string{name()} + "' to implemented"); + throwIfError(err, "Couldn't set module '" + name() + "' to implemented"); } /** @@ -133,7 +133,7 @@ void Module::setImplemented(std::vector features) }); auto err = lys_set_implemented(m_module, featuresArray.get()); - throwIfError(err, "Couldn't set module '" + std::string{name()} + "' to implemented"); + throwIfError(err, "Couldn't set module '" + name() + "' to implemented"); } /** @@ -194,7 +194,7 @@ ChildInstanstiables Module::childInstantiables() const std::vector Module::extensionInstances() const { if (!m_module->compiled) { - throw Error{"Module \"" + std::string{this->name()} + "\" not implemented"}; + throw Error{"Module \"" + this->name() + "\" not implemented"}; } std::vector res; @@ -208,7 +208,7 @@ std::vector Module::extensionInstances() const ExtensionInstance Module::extensionInstance(const std::string& name) const { if (!m_module->compiled) { - throw Error{"Module \"" + std::string{this->name()} + "\" not implemented"}; + throw Error{"Module \"" + this->name() + "\" not implemented"}; } auto span = std::span(m_module->compiled->exts, LY_ARRAY_COUNT(m_module->compiled->exts)); @@ -216,7 +216,7 @@ ExtensionInstance Module::extensionInstance(const std::string& name) const return ext.argument == name; }); if (it == span.end()) { - throw Error{"Extension \""s + name + "\" not defined in module \"" + std::string{this->name()} + "\""}; + throw Error{"Extension \""s + name + "\" not defined in module \"" + this->name() + "\""}; } return ExtensionInstance(&*it, m_ctx); } @@ -278,7 +278,7 @@ Feature::Feature(const lysp_feature* feature, std::shared_ptr ctx) * * Wraps `lysp_feature::name`. */ -std::string_view Feature::name() const +std::string Feature::name() const { return m_feature->name; } @@ -351,7 +351,7 @@ Module Identity::module() const * * Wraps `lysc_ident::name` */ -std::string_view Identity::name() const +std::string Identity::name() const { return m_ident->name; } @@ -372,7 +372,7 @@ ExtensionInstance::ExtensionInstance(const lysc_ext_instance* ext, std::shared_p * * Wraps `lysc_ext_instance::argument`. */ -std::string_view ExtensionInstance::argument() const +std::string ExtensionInstance::argument() const { return m_ext->argument; } @@ -398,7 +398,7 @@ Extension::Extension(const lysc_ext* ext, std::shared_ptr ctx) * * Wraps `lysc_ext::name`. */ -std::string_view Extension::name() const +std::string Extension::name() const { return m_ext->name; } diff --git a/src/SchemaNode.cpp b/src/SchemaNode.cpp index 6d052db3..882c1268 100644 --- a/src/SchemaNode.cpp +++ b/src/SchemaNode.cpp @@ -69,7 +69,7 @@ std::string SchemaNode::path() const * * Wraps `lysc_node::name`. */ -std::string_view SchemaNode::name() const +std::string SchemaNode::name() const { return m_node->name; } @@ -123,7 +123,7 @@ Collection SchemaNode::immediateChildren() c * * Wraps `lysc_node::dsc`. */ -std::optional SchemaNode::description() const +std::optional SchemaNode::description() const { if (!m_node->dsc) { return std::nullopt; @@ -149,7 +149,7 @@ Status SchemaNode::status() const return Status::Obsolete; } - throw Error(std::string{"Couldn't retrieve the status of '"} + path()); + throw Error("Couldn't retrieve the status of '" + path()); } /** @@ -167,7 +167,7 @@ Config SchemaNode::config() const return Config::False; } - throw Error(std::string{"Couldn't retrieve config value of '"} + path()); + throw Error("Couldn't retrieve config value of '" + path()); } /** @@ -197,7 +197,7 @@ NodeType SchemaNode::nodeType() const Container SchemaNode::asContainer() const { if (nodeType() != NodeType::Container) { - throw Error("Schema node is not a container: " + std::string{path()}); + throw Error("Schema node is not a container: " + path()); } return Container{m_node, m_ctx}; @@ -210,7 +210,7 @@ Container SchemaNode::asContainer() const Leaf SchemaNode::asLeaf() const { if (nodeType() != NodeType::Leaf) { - throw Error("Schema node is not a leaf: " + std::string{path()}); + throw Error("Schema node is not a leaf: " + path()); } return Leaf{m_node, m_ctx}; @@ -223,7 +223,7 @@ Leaf SchemaNode::asLeaf() const LeafList SchemaNode::asLeafList() const { if (nodeType() != NodeType::Leaflist) { - throw Error("Schema node is not a leaf-list: " + std::string{path()}); + throw Error("Schema node is not a leaf-list: " + path()); } return LeafList{m_node, m_ctx}; @@ -236,7 +236,7 @@ LeafList SchemaNode::asLeafList() const List SchemaNode::asList() const { if (nodeType() != NodeType::List) { - throw Error("Schema node is not a list: " + std::string{path()}); + throw Error("Schema node is not a list: " + path()); } return List{m_node, m_ctx}; @@ -277,7 +277,7 @@ std::optional SchemaNode::parent() const ActionRpc SchemaNode::asActionRpc() const { if (auto type = nodeType(); type != NodeType::RPC && type != NodeType::Action) { - throw Error("Schema node is not an action or an RPC: " + std::string{path()}); + throw Error("Schema node is not an action or an RPC: " + path()); } return ActionRpc{m_node, m_ctx}; @@ -290,7 +290,7 @@ ActionRpc SchemaNode::asActionRpc() const AnyDataAnyXML SchemaNode::asAnyDataAnyXML() const { if (auto type = nodeType(); type != NodeType::AnyData && type != NodeType::AnyXML) { - throw Error("Schema node is not an anydata or an anyxml: " + std::string{path()}); + throw Error("Schema node is not an anydata or an anyxml: " + path()); } return AnyDataAnyXML{m_node, m_ctx}; @@ -325,7 +325,7 @@ When::When(const lysc_when* when, std::shared_ptr ctx) * * Wraps `lysc_when::cond`. */ -std::string_view When::condition() const +std::string When::condition() const { return lyxp_get_expr(m_when->cond); } @@ -337,7 +337,7 @@ std::string_view When::condition() const * * Wraps `lysc_when::dsc`. */ -std::optional When::description() const +std::optional When::description() const { if (!m_when->dsc) { return std::nullopt; @@ -464,7 +464,7 @@ types::Type LeafList::valueType() const * * Wraps `lysc_node_leaf::units`. */ -std::optional Leaf::units() const +std::optional Leaf::units() const { auto units = reinterpret_cast(m_node)->units; if (!units) { @@ -520,7 +520,7 @@ libyang::types::constraints::ListSize LeafList::minElements() const * * Wraps `lysc_node_leaflist::units`. */ -std::optional LeafList::units() const +std::optional LeafList::units() const { auto units = reinterpret_cast(m_node)->units; if (!units) { @@ -546,11 +546,11 @@ bool LeafList::isUserOrdered() const * * Wraps `lysc_node_leaf::dflt`. */ -std::optional Leaf::defaultValueStr() const +std::optional Leaf::defaultValueStr() const { auto dflt = reinterpret_cast(m_node)->dflt; if (dflt) { - return std::string_view{lyd_value_get_canonical(m_ctx.get(), dflt)}; + return lyd_value_get_canonical(m_ctx.get(), dflt); } else { return std::nullopt; } diff --git a/src/Type.cpp b/src/Type.cpp index ec070d0a..b1827645 100644 --- a/src/Type.cpp +++ b/src/Type.cpp @@ -189,7 +189,7 @@ std::vector Enumeration::items() const * * Wraps `lysp_type::name`. */ -std::string_view Type::name() const +std::string Type::name() const { throwIfParsedUnavailable(); @@ -201,7 +201,7 @@ std::string_view Type::name() const * * This method only works if the associated context was created with the libyang::ContextOptions::SetPrivParsed flag. */ -std::optional Type::description() const +std::optional Type::description() const { throwIfParsedUnavailable(); @@ -269,7 +269,7 @@ std::vector IdentityRef::bases() const * * Wraps `lyxp_get_expr`. */ -std::string_view LeafRef::path() const +std::string LeafRef::path() const { auto lref = reinterpret_cast(m_type); return lyxp_get_expr(lref->path); diff --git a/src/Utils.cpp b/src/Utils.cpp index 9ab02a42..4dd9006e 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -111,7 +111,7 @@ template std::string LIBYANG_CPP_EXPORT ValuePrinter::operator()(const std::stri std::string qualifiedName(const Identity& identity) { - return std::string{identity.module().name()} + ':' + std::string{identity.name()}; + return identity.module().name() + ':' + identity.name(); } InstanceIdentifier::InstanceIdentifier(const std::string& path, const std::optional& node) diff --git a/tests/data_node.cpp b/tests/data_node.cpp index 2d279794..ce114587 100644 --- a/tests/data_node.cpp +++ b/tests/data_node.cpp @@ -1949,11 +1949,11 @@ TEST_CASE("Data Node manipulation") data->newPath("/example-schema:myRpc/another", "yay", libyang::CreationOptions::Output); DOCTEST_SUBCASE("JSON") { - out = ctx.newOpaqueJSON(std::string(data->schema().module().name()), "output", std::nullopt); + out = ctx.newOpaqueJSON(data->schema().module().name(), "output", std::nullopt); } DOCTEST_SUBCASE("XML") { - out = ctx.newOpaqueXML(std::string(data->schema().module().ns()), "output", std::nullopt); + out = ctx.newOpaqueXML(data->schema().module().ns(), "output", std::nullopt); } REQUIRE(out); diff --git a/tests/pretty_printers.hpp b/tests/pretty_printers.hpp index 6bbc16fb..1c2a8ad6 100644 --- a/tests/pretty_printers.hpp +++ b/tests/pretty_printers.hpp @@ -71,13 +71,13 @@ doctest::String toString(const std::vector& errors) return oss.str().c_str(); } -doctest::String toString(const std::optional& optString) +doctest::String toString(const std::optional& optString) { if (!optString) { return "std::nullopt"; } - return std::string{*optString}.c_str(); + return optString->c_str(); } doctest::String toString(const std::optional& optTree) From 3f8a5aa4fd96bf0fd0839e4b8e7df73f4dc3400c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= Date: Sun, 24 Mar 2024 21:14:35 +0100 Subject: [PATCH 03/57] libyang v3: wrap stored typedef names This will make it possible to get rid of that rather ugly hack in velia (or anywhere else when people need to know which of the possible union options is used). Change-Id: Id5639c85a07014609304f48b255107637b1f8b49 --- include/libyang-cpp/Type.hpp | 1 + src/Type.cpp | 14 ++++++++++++++ tests/data_node.cpp | 6 +++++- 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/include/libyang-cpp/Type.hpp b/include/libyang-cpp/Type.hpp index faa1b2dc..f4aff97a 100644 --- a/include/libyang-cpp/Type.hpp +++ b/include/libyang-cpp/Type.hpp @@ -66,6 +66,7 @@ class LIBYANG_CPP_EXPORT Type { InstanceIdentifier asInstanceIdentifier() const; std::string name() const; + std::optional typedefName() const; std::optional description() const; std::string internalPluginId() const; diff --git a/src/Type.cpp b/src/Type.cpp index b1827645..01b2c4f2 100644 --- a/src/Type.cpp +++ b/src/Type.cpp @@ -196,6 +196,20 @@ std::string Type::name() const return m_typeParsed->name; } +/** + * @brief Name of the typedef, if avaialable + * + * Wraps `lysc_type::name`. + */ +std::optional Type::typedefName() const +{ + if (!m_type->name) { + return std::nullopt; + } + + return m_type->name; +} + /** * @brief Returns the description of the type. * diff --git a/tests/data_node.cpp b/tests/data_node.cpp index ce114587..0df8319f 100644 --- a/tests/data_node.cpp +++ b/tests/data_node.cpp @@ -2226,7 +2226,7 @@ TEST_CASE("union data types") std::optional ctxWithParsed{std::in_place, std::nullopt, libyang::ContextOptions::SetPrivParsed | libyang::ContextOptions::NoYangLibrary | libyang::ContextOptions::DisableSearchCwd}; ctxWithParsed->parseModule(with_inet_types_module, libyang::SchemaFormat::YANG); - std::string input, expectedPlugin; + std::string input, expectedPlugin, expectedTypedef; DOCTEST_SUBCASE("IPv6") { @@ -2243,21 +2243,25 @@ TEST_CASE("union data types") input = "::ffff:192.0.2.1"; } expectedPlugin = "ipv6"; + expectedTypedef = "ipv6-address"; } DOCTEST_SUBCASE("IPv4") { input = "127.0.0.1"; expectedPlugin = "ipv4"; + expectedTypedef = "ipv4-address"; } DOCTEST_SUBCASE("string") { input = "foo-bar.example.org"; expectedPlugin = "string"; + expectedTypedef = "domain-name"; } auto node = ctxWithParsed->newPath("/with-inet-types:hostname", input); REQUIRE(node.asTerm().valueType().internalPluginId().find(expectedPlugin) != std::string::npos); REQUIRE_THROWS_AS(node.asTerm().valueType().name(), libyang::ParsedInfoUnavailable); + REQUIRE(node.asTerm().valueType().typedefName() == expectedTypedef); } From 39db170e631cc17c3ec4e0a84a6e661e3a1cae4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= Date: Wed, 10 Apr 2024 11:55:01 +0200 Subject: [PATCH 04/57] version bump for libyang v3 First of all, it used to be called v3, but it got released as v2.2, and then later bumped to v3, yay. Since we've cleaned up our API a bit, let's mark this one as v2. Hopefully this should make it clear that we do not implement any semver guarantees and that we are not committing to stable API/ABI just yet. Change-Id: I6a0c5deb605597ef8d27120be9376761b9b2e904 --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 6e547075..0b286db1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -28,8 +28,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>=2.2.3 IMPORTED_TARGET) -set(LIBYANG_CPP_PKG_VERSION "1.1.0") +pkg_check_modules(LIBYANG REQUIRED libyang>=3.0.1 IMPORTED_TARGET) +set(LIBYANG_CPP_PKG_VERSION "2") include_directories(${CMAKE_CURRENT_SOURCE_DIR}/include) From fcefc214b75b5aea21f840127609de52fee54f4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= Date: Tue, 21 May 2024 14:50:09 +0200 Subject: [PATCH 05/57] tests: add missing includes for gcc14 Change-Id: I57f105f5372f3bbb9c814d7e5b78759f942c4a99 --- src/Context.cpp | 1 + tests/pretty_printers.hpp | 1 + 2 files changed, 2 insertions(+) diff --git a/src/Context.cpp b/src/Context.cpp index 713c50fb..cae5280b 100644 --- a/src/Context.cpp +++ b/src/Context.cpp @@ -6,6 +6,7 @@ * SPDX-License-Identifier: BSD-3-Clause */ +#include #include #include #include diff --git a/tests/pretty_printers.hpp b/tests/pretty_printers.hpp index 1c2a8ad6..e231e747 100644 --- a/tests/pretty_printers.hpp +++ b/tests/pretty_printers.hpp @@ -7,6 +7,7 @@ */ #pragma once +#include #include #include #include From a863b8ddd7df44e7c7dfd82076d63092d63f9daf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= Date: Tue, 21 May 2024 15:28:40 +0200 Subject: [PATCH 06/57] tests: ignore return value of a function In gcc14, this compile error appeared: libyang-cpp/tests/data_node.cpp:1286:13: error: ignoring return value of function declared with 'nodiscard' attribute [-Werror,-Wunused-result] 1286 | std::any_of(set.begin(), set.end(), [](const auto& node) { node.path(); return true; }); It seems that gcc now starts complaining about std::any_of's nodiscard attribute. We can fix this error by assigning the return value to std::ignore [1]. [1] https://stackoverflow.com/a/77195811/2245623 Change-Id: Ia37efd6f8842d91c7b97a96f864c318739f0dd71 --- tests/data_node.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/data_node.cpp b/tests/data_node.cpp index 0df8319f..34ab1788 100644 --- a/tests/data_node.cpp +++ b/tests/data_node.cpp @@ -1283,7 +1283,7 @@ TEST_CASE("Data Node manipulation") { auto set = node->findXPath("/example-schema:person[name='Dan']"); - std::any_of(set.begin(), set.end(), [](const auto& node) { node.path(); return true; }); + std::ignore = std::any_of(set.begin(), set.end(), [](const auto& node) { node.path(); return true; }); } DOCTEST_SUBCASE("find one node") From e895a522a97c72f7a8ab4bf4e488b8f76f823ee6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= Date: Mon, 27 May 2024 17:56:23 +0200 Subject: [PATCH 07/57] compare Module instances I want to compare if a meta entry's module is the same as a module I have. The comparison is implemented on the value of lys_module*. It seems that successive calls to ly_ctx_get_module always return the same pointer so it *should* be safe. However, same modules in different contexts compare as not equal. Change-Id: I96efca783d5e780e98693330803e73847151d7af --- include/libyang-cpp/Module.hpp | 2 ++ src/Module.cpp | 5 +++++ tests/context.cpp | 19 +++++++++++++++++++ 3 files changed, 26 insertions(+) diff --git a/include/libyang-cpp/Module.hpp b/include/libyang-cpp/Module.hpp index 1085d48d..3d213eaa 100644 --- a/include/libyang-cpp/Module.hpp +++ b/include/libyang-cpp/Module.hpp @@ -89,6 +89,8 @@ class LIBYANG_CPP_EXPORT Module { std::string printStr(const SchemaOutputFormat format, const std::optional flags = std::nullopt, std::optional lineLength = std::nullopt) const; + bool operator==(const Module& other) const; + friend Context; friend DataNode; friend Meta; diff --git a/src/Module.cpp b/src/Module.cpp index 6df4f083..edbeb339 100644 --- a/src/Module.cpp +++ b/src/Module.cpp @@ -231,6 +231,11 @@ std::string Module::printStr(const SchemaOutputFormat format, const std::optiona return printModule(lys_print_module, m_module, format, flags, lineLength, "lys_print_module"); } +bool Module::operator==(const Module& other) const +{ + return m_module == other.m_module; +} + SubmoduleParsed::SubmoduleParsed(const lysp_submodule* submodule, std::shared_ptr ctx) : m_ctx(ctx) , m_submodule(submodule) diff --git a/tests/context.cpp b/tests/context.cpp index cb629ef1..f5641b44 100644 --- a/tests/context.cpp +++ b/tests/context.cpp @@ -369,6 +369,25 @@ TEST_CASE("context") REQUIRE(modules.at(7).revision() == std::nullopt); } + DOCTEST_SUBCASE("Module comparison") + { + ctx->setSearchDir(TESTS_DIR / "yang"); + auto mod = ctx->loadModule("mod1", std::nullopt, {}); + + DOCTEST_SUBCASE("Same module loaded later") + { + REQUIRE(mod == ctx->loadModule("mod1", std::nullopt, {})); + } + + DOCTEST_SUBCASE("Same module, different contexts") + { + std::optional ctx2{std::in_place, std::nullopt, libyang::ContextOptions::NoYangLibrary | libyang::ContextOptions::DisableSearchCwd}; + ctx2->setSearchDir(TESTS_DIR / "yang"); + + REQUIRE(mod != ctx2->loadModule("mod1", std::nullopt, {})); + } + } + DOCTEST_SUBCASE("Context::registerModuleCallback") { auto numCalled = 0; From 1495125e96435e641a2dd39903a78c4e57258ad6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= Date: Wed, 29 May 2024 14:01:38 +0200 Subject: [PATCH 08/57] Assign collection to Collection::end() iterator In the current code, whenever I try to compare two Collection::end() I receive an exception saying "Iterator is invalid". This is caused by the fact that that operator== checks whether the current iterator has a collection assigned. However, the end() iterator was always constructed without the pointer to the collection. The fix assigns collection to the end() iterator. Because end() iterator is represented as a nullptr, two end() iterators to two different collections would compare as identical. Therefore, the Iterator::op== now also takes the collection into account. Change-Id: I77dc05a11a2ce610317ab14615cc619b07dd1a07 --- include/libyang-cpp/Collection.hpp | 2 +- src/Collection.cpp | 11 ++++++++--- tests/data_node.cpp | 13 +++++++++++++ 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/include/libyang-cpp/Collection.hpp b/include/libyang-cpp/Collection.hpp index dd09f3ae..557a0a2c 100644 --- a/include/libyang-cpp/Collection.hpp +++ b/include/libyang-cpp/Collection.hpp @@ -70,7 +70,7 @@ class LIBYANG_CPP_EXPORT Iterator { private: Iterator(underlying_node_t* start, const Collection* coll); - Iterator(const end); + Iterator(const Collection* coll, const end); underlying_node_t* m_current; underlying_node_t* m_start; diff --git a/src/Collection.cpp b/src/Collection.cpp index b68f48a0..f3ef2579 100644 --- a/src/Collection.cpp +++ b/src/Collection.cpp @@ -30,9 +30,9 @@ Iterator::Iterator(underlying_node_t* start, cons * @brief Creates an iterator that acts as the `end()` for iteration. */ template -Iterator::Iterator(const end) +Iterator::Iterator(const Collection* coll, const end) : m_current(nullptr) - , m_collection(nullptr) + , m_collection(coll) { } @@ -155,6 +155,11 @@ template bool Iterator::operator==(const Iterator& it) const { throwIfInvalid(); + + if (m_collection != it.m_collection) { + throw std::out_of_range("Iterators are from different collections"); + } + return m_current == it.m_current; } @@ -276,7 +281,7 @@ template Iterator Collection::end() const { throwIfInvalid(); - return Iterator{typename Iterator::end{}}; + return Iterator{this, typename Iterator::end{}}; } template diff --git a/tests/data_node.cpp b/tests/data_node.cpp index 34ab1788..a398fed8 100644 --- a/tests/data_node.cpp +++ b/tests/data_node.cpp @@ -1045,12 +1045,25 @@ TEST_CASE("Data Node manipulation") } } + DOCTEST_SUBCASE("End iterators") + { + auto coll = node->childrenDfs(); + REQUIRE(coll.end() == coll.end()); + + auto leafColl = node->findPath("/example-schema:bigTree/one/myLeaf")->childrenDfs(); + REQUIRE(++leafColl.begin() == leafColl.end()); + + REQUIRE_THROWS_WITH_AS(coll.end().operator==(leafColl.end()), "Iterators are from different collections", std::out_of_range); + } + DOCTEST_SUBCASE("standard algorithms") { auto coll = node->childrenDfs(); REQUIRE(std::find_if(coll.begin(), coll.end(), [] (const auto& node) { return node.path() == "/example-schema:bigTree/two/myList[thekey='432']/thekey"; }) != coll.end()); + + REQUIRE(std::all_of(coll.begin(), coll.end(), [](const auto&) { return true; })); } DOCTEST_SUBCASE("incrementing") From d740b7c864d7120d73b8fc1c0f9e9545a81cf13e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= Date: Wed, 29 May 2024 14:07:49 +0200 Subject: [PATCH 09/57] Iterator operator== is now symmetric The code only checked if the lhs iterator is invalidated, but not the rhs iterator. I *think* this was because the rhs iterator could have been .end() and that was always invalid. This is no longer true. Change-Id: Ibf9dd3b83c4eef575506d7550fc338d277e33f20 --- src/Collection.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Collection.cpp b/src/Collection.cpp index f3ef2579..9f97b456 100644 --- a/src/Collection.cpp +++ b/src/Collection.cpp @@ -155,6 +155,7 @@ template bool Iterator::operator==(const Iterator& it) const { throwIfInvalid(); + it.throwIfInvalid(); if (m_collection != it.m_collection) { throw std::out_of_range("Iterators are from different collections"); From d8bf2a8c80e912e3e2ed312ad1ede22b7f03fb18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= Date: Mon, 3 Jun 2024 12:49:53 +0200 Subject: [PATCH 10/57] Wrap lyd_meta_is_internal to check if meta is internal There are some internal meta attributes that libyang uses for internal stuff and they are ignored when printing the meta collections. One (and now also the only) example of such node is yang:lyds_tree attribute. It can be useful to check if the meta attribute is internal. For instance, when we want to be sure that our parsed data did not contain any meta attributes from user. However, sometimes, libyang silently inserts these internal attributes while parsing. We asked upstream if there could be a function that checks for us if the attribute is internal and therefore we should ignore it. They kindly provided us with such function (`lyd_meta_is_internal`) [1]. This commit wraps the function. Unfortunately, our Meta class does not wrap lyd_meta, only copies data from the structure so we can't just call this function when requested but we call it in Meta object construction which creates a little overhead in both time and memory. [1] https://github.com/CESNET/libyang/commit/f77ca9cf807eeb11c8f806b2c32fb0a21fb4a22f Change-Id: Id772ded765569089525cf4fd5dba8a64efe87d02 --- CMakeLists.txt | 2 +- include/libyang-cpp/DataNode.hpp | 2 ++ src/DataNode.cpp | 7 +++++++ tests/data_node.cpp | 23 +++++++++++++++++++++++ tests/example_schema.hpp | 4 ++++ 5 files changed, 37 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 0b286db1..d1003d25 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.0.1 IMPORTED_TARGET) +pkg_check_modules(LIBYANG REQUIRED libyang>=3.0.11 IMPORTED_TARGET) set(LIBYANG_CPP_PKG_VERSION "2") include_directories(${CMAKE_CURRENT_SOURCE_DIR}/include) diff --git a/include/libyang-cpp/DataNode.hpp b/include/libyang-cpp/DataNode.hpp index 8f9d087f..839c33f5 100644 --- a/include/libyang-cpp/DataNode.hpp +++ b/include/libyang-cpp/DataNode.hpp @@ -183,6 +183,7 @@ class LIBYANG_CPP_EXPORT Meta { std::string name() const; std::string valueStr() const; Module module() const; + bool isInternal() const; private: friend Iterator; @@ -191,6 +192,7 @@ class LIBYANG_CPP_EXPORT Meta { std::string m_name; std::string m_value; Module m_mod; + bool m_isInternal; }; diff --git a/src/DataNode.cpp b/src/DataNode.cpp index b24a4ed6..bcad13eb 100644 --- a/src/DataNode.cpp +++ b/src/DataNode.cpp @@ -981,6 +981,7 @@ Meta::Meta(lyd_meta* meta, std::shared_ptr ctx) : m_name(meta->name) , m_value(lyd_get_meta_value(meta)) , m_mod(meta->annotation->module, ctx) + , m_isInternal(lyd_meta_is_internal(meta)) { } @@ -999,6 +1000,12 @@ Module Meta::module() const return m_mod; } +/** @brief Checks if the meta attribute is considered internal for libyang, see `lyd_meta_is_internal` */ +bool Meta::isInternal() const +{ + return m_isInternal; +} + /** * Creates a JSON attribute for an opaque data node. * Wraps `lyd_new_attr`. diff --git a/tests/data_node.cpp b/tests/data_node.cpp index a398fed8..e3350e1d 100644 --- a/tests/data_node.cpp +++ b/tests/data_node.cpp @@ -1899,6 +1899,7 @@ TEST_CASE("Data Node manipulation") } auto meta = netconfDeletePresenceCont.meta(); + REQUIRE(std::none_of(meta.begin(), meta.end(), [](const auto& meta) { return meta.isInternal(); })); std::transform(meta.begin(), meta.end(), std::back_inserter(actual), [] (const auto& it) { return std::pair{it.name(), it.valueStr()}; }); REQUIRE(actual == expected); } @@ -1976,6 +1977,28 @@ TEST_CASE("Data Node manipulation") REQUIRE(*out->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::WithSiblings) == expectedJson); REQUIRE(*out->printStr(libyang::DataFormat::XML, libyang::PrintFlags::WithSiblings) == expectedXml); } + + DOCTEST_SUBCASE("libyang internal metadata") + { + /* + * - currently, libyang creates an internal meta node yang:lyds_tree representing a RB-tree for ordering of data in lists + * - this test depends on internal libyang implementation which can of course change anytime + * - but it seems that yang:lyds_tree can't be created manually (there is no valid value for it?) and I don't know how to test this otherwise + */ + DOCTEST_SUBCASE("leaf-list ordered by system") + { + auto node = ctx.parseData(R"({"example-schema3:valuesOrderedBySystem": [1,2,3]})"s, libyang::DataFormat::JSON, libyang::ParseOptions::Strict | libyang::ParseOptions::NoState | libyang::ParseOptions::ParseOnly); + const auto metaColl = node->meta(); + REQUIRE(std::find_if(metaColl.begin(), metaColl.end(), [](const auto& meta) { return meta.isInternal(); }) != metaColl.end()); + } + + DOCTEST_SUBCASE("leaf-list ordered by user") + { + auto node = ctx.parseData(R"({"example-schema3:values": [1,2,3]})"s, libyang::DataFormat::JSON, libyang::ParseOptions::Strict | libyang::ParseOptions::NoState | libyang::ParseOptions::ParseOnly); + const auto metaColl = node->meta(); + REQUIRE(std::find_if(metaColl.begin(), metaColl.end(), [](const auto& meta) { return meta.isInternal(); }) == metaColl.end()); + } + } } DOCTEST_SUBCASE("Extension nodes") diff --git a/tests/example_schema.hpp b/tests/example_schema.hpp index 52a316c9..c3892550 100644 --- a/tests/example_schema.hpp +++ b/tests/example_schema.hpp @@ -293,6 +293,10 @@ module example-schema3 { type int32; } + leaf-list valuesOrderedBySystem { + type int32; + } + list person { key 'name'; leaf name { From c5ecde0af10c0da8cc6080cc05388d311f29c5c2 Mon Sep 17 00:00:00 2001 From: Edoardo Bortolozzo Date: Tue, 28 May 2024 11:18:42 +0200 Subject: [PATCH 11/57] fix(DataNode): segfault when unlink unmanaged When an Unmanaged DataNode is unlinked (e.g. was temporary linked to print a JSON string), the method dereferences a nullptr which is UB. Added tests in unsafe.cpp. Fixes: https://github.com/CESNET/libyang-cpp/issues/21 Change-Id: Ia6a621e0b9d5faad8c64368b3a2e344e52288e87 --- src/DataNode.cpp | 4 ++-- tests/unsafe.cpp | 34 ++++++++++++++++++++++++++++++++-- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/src/DataNode.cpp b/src/DataNode.cpp index bcad13eb..f7a2772e 100644 --- a/src/DataNode.cpp +++ b/src/DataNode.cpp @@ -641,7 +641,7 @@ void DataNode::unlink() { handleLyTreeOperation(this, [this] () { lyd_unlink_tree(m_node); - }, OperationScope::JustThisNode, std::make_shared(m_refs->context)); + }, OperationScope::JustThisNode, std::make_shared(m_refs ? m_refs->context : nullptr)); } /** @@ -676,7 +676,7 @@ void DataNode::unlinkWithSiblings() { handleLyTreeOperation(this, [this] { lyd_unlink_siblings(m_node); - }, OperationScope::AffectsFollowingSiblings, std::make_shared(m_refs->context)); + }, OperationScope::AffectsFollowingSiblings, std::make_shared(m_refs ? m_refs->context : nullptr)); } /** diff --git a/tests/unsafe.cpp b/tests/unsafe.cpp index 4988c080..0383f5cc 100644 --- a/tests/unsafe.cpp +++ b/tests/unsafe.cpp @@ -4,12 +4,13 @@ * Written by Václav Kubernát * * SPDX-License-Identifier: BSD-3-Clause -*/ + */ #include #include #include #include +#include #include "example_schema.hpp" #include "test_vars.hpp" #include "utils/filesystem_path.hpp" @@ -40,7 +41,6 @@ TEST_CASE("Unsafe methods") ctx_deleter.release(); auto wrapped = libyang::createUnmanagedContext(ctx, ly_ctx_destroy); - } DOCTEST_SUBCASE("No custom deleter") @@ -111,6 +111,20 @@ TEST_CASE("Unsafe methods") // Both are still unmanaged, both are accessible. REQUIRE(wrapped.path() == "/example-schema:leafInt32"); REQUIRE(anotherNodeWrapped.path() == "/example-schema:leafInt8"); + + DOCTEST_SUBCASE("no explicit unlink") { } + + DOCTEST_SUBCASE("unlink an unmanaged node from an unmanaged node") + { + REQUIRE(wrapped.findPath("/example-schema:leafInt8")); + REQUIRE(anotherNodeWrapped.findPath("/example-schema:leafInt32")); + + // After unlink they are not reachable from each other + anotherNodeWrapped.unlink(); + REQUIRE(!wrapped.findPath("/example-schema:leafInt8")); + REQUIRE(!anotherNodeWrapped.findPath("/example-schema:leafInt32")); + lyd_free_all(anotherNode); + } } // You have a C++ managed node and you want to insert that into an unmanaged node. @@ -123,6 +137,22 @@ TEST_CASE("Unsafe methods") // BOTH are now unmanaged, both are accessible. REQUIRE(wrapped.path() == "/example-schema:leafInt32"); REQUIRE(anotherNodeWrapped.path() == "/example-schema:leafInt8"); + + DOCTEST_SUBCASE("no explicit unlink") { } + + DOCTEST_SUBCASE("unlink a managed node from an unmanaged node") + { + REQUIRE(wrapped.findPath("/example-schema:leafInt8")); + REQUIRE(anotherNodeWrapped.findPath("/example-schema:leafInt32")); + + // After unlink they are not reachable from each other + anotherNodeWrapped.unlink(); + REQUIRE(!wrapped.findPath("/example-schema:leafInt8")); + REQUIRE(!anotherNodeWrapped.findPath("/example-schema:leafInt32")); + + // this is still unmanaged and we need to delete it + lyd_free_all(anotherNode); + } } // You have a C++ managed node and you want to insert an unmanaged node into it. From 92049a8445e7e32f5f3f1ee3eb98e22f21b94118 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= Date: Tue, 11 Jun 2024 20:18:33 +0200 Subject: [PATCH 12/57] Allow parsing standalone notifications in Context::parseOp Even when the libyang docs is not explicit on that, lyd_parse_op is used for parsing standalone notifications too. For such case, the lyd_node* tree and op are the same, or, they point into the nodes in the same tree. So we should not wrap the pointers separately (this would result in a heap-after-use). Change-Id: Ib94114193a1a9073d9a4c6d15522eb41ba9c425d --- src/Context.cpp | 20 ++++-- tests/data_node.cpp | 144 +++++++++++++++++++++++++++++---------- tests/example_schema.hpp | 6 ++ 3 files changed, 128 insertions(+), 42 deletions(-) diff --git a/src/Context.cpp b/src/Context.cpp index cae5280b..f49b2091 100644 --- a/src/Context.cpp +++ b/src/Context.cpp @@ -187,7 +187,8 @@ std::optional Context::parseData( * * - a NETCONF RPC, * - a NETCONF notification, - * - a RESTCONF notification. + * - a RESTCONF notification, + * - a YANG notification. * * 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. @@ -211,14 +212,21 @@ ParsedOp Context::parseOp(const std::string& input, const DataFormat format, con switch (opType) { case OperationType::RpcNetconf: case OperationType::NotificationNetconf: - case OperationType::NotificationRestconf: { + case OperationType::NotificationRestconf: + case OperationType::NotificationYang: { lyd_node* op = nullptr; lyd_node* tree = nullptr; auto err = lyd_parse_op(m_ctx.get(), nullptr, in.get(), utils::toLydFormat(format), utils::toOpType(opType), &tree, &op); - ParsedOp res { - .tree = tree ? std::optional{libyang::wrapRawNode(tree)} : std::nullopt, - .op = op ? std::optional{libyang::wrapRawNode(op)} : std::nullopt - }; + + ParsedOp res; + res.tree = tree ? std::optional{libyang::wrapRawNode(tree)} : std::nullopt; + + if (opType == OperationType::NotificationYang) { + 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; + } + throwIfError(err, "Can't parse a standalone rpc/action/notification into operation data tree"); return res; } diff --git a/tests/data_node.cpp b/tests/data_node.cpp index e3350e1d..812eb27f 100644 --- a/tests/data_node.cpp +++ b/tests/data_node.cpp @@ -2062,48 +2062,117 @@ TEST_CASE("Data Node manipulation") ctx.loadModule("ietf-netconf-nmda"); DOCTEST_SUBCASE("notifications") { - std::string payload; - auto opType = libyang::OperationType::DataYang; + DOCTEST_SUBCASE("restconf/netconf") { + std::string payload; + auto opType = libyang::OperationType::DataYang; - DOCTEST_SUBCASE("RESTCONF JSON") { - payload = R"( - { - "ietf-restconf:notification" : { - "eventTime" : "2013-12-21T00:01:00Z", - "example-schema:event" : { - "event-class" : "fault" + DOCTEST_SUBCASE("RESTCONF JSON") { + payload = R"( + { + "ietf-restconf:notification" : { + "eventTime" : "2013-12-21T00:01:00Z", + "example-schema:event" : { + "event-class" : "fault" + } + } } - } + )"; + opType = libyang::OperationType::NotificationRestconf; } - )"; - opType = libyang::OperationType::NotificationRestconf; - } - - DOCTEST_SUBCASE("NETCONF XML") { - payload = R"( - - 2013-12-21T00:01:00Z - - fault - - - )"; - opType = libyang::OperationType::NotificationNetconf; + + DOCTEST_SUBCASE("NETCONF XML") { + payload = R"( + + 2013-12-21T00:01:00Z + + fault + + + )"; + opType = libyang::OperationType::NotificationNetconf; + } + + auto notif = ctx.parseOp(payload, dataTypeFor(payload), opType); + REQUIRE(notif.tree); + REQUIRE(notif.tree->path() == "/notification"); + auto node = notif.tree->child(); + REQUIRE(node); + REQUIRE(node->path() == "/notification/eventTime"); + REQUIRE(node->asOpaque().value() == "2013-12-21T00:01:00Z"); + + REQUIRE(notif.op); + node = notif.op->findPath("/example-schema:event/event-class"); + REQUIRE(!!node); + REQUIRE(std::visit(libyang::ValuePrinter{}, node->asTerm().value()) == "fault"); } - auto notif = ctx.parseOp(payload, dataTypeFor(payload), opType); - REQUIRE(notif.tree); - REQUIRE(notif.tree->path() == "/notification"); - auto node = notif.tree->child(); - REQUIRE(node); - REQUIRE(node->path() == "/notification/eventTime"); - REQUIRE(node->asOpaque().value() == "2013-12-21T00:01:00Z"); + DOCTEST_SUBCASE("yang") + { + std::string payload; - REQUIRE(notif.op); - node = notif.op->findPath("/example-schema:event/event-class"); - REQUIRE(!!node); - REQUIRE(std::visit(libyang::ValuePrinter{}, node->asTerm().value()) == "fault"); + DOCTEST_SUBCASE("top-level") + { + DOCTEST_SUBCASE("json") + { + payload = R"({ + "example-schema:event" : { + "event-class" : "fault" + } + })"; + } + DOCTEST_SUBCASE("xml") + { + payload = R"( + + fault + + )"; + } + auto notif = ctx.parseOp(payload, dataTypeFor(payload), libyang::OperationType::NotificationYang); + REQUIRE(notif.tree); + REQUIRE(notif.op); + REQUIRE(notif.op == notif.tree); + REQUIRE(notif.tree->path() == "/example-schema:event"); + auto node = notif.op->findPath("/example-schema:event/event-class"); + REQUIRE(!!node); + REQUIRE(std::visit(libyang::ValuePrinter{}, node->asTerm().value()) == "fault"); + } + + DOCTEST_SUBCASE("nested") + { + DOCTEST_SUBCASE("json") + { + payload = R"({ + "example-schema:person" : [{ + "name": "John", + "event": { + "description" : "fault" + } + }] + })"; + } + DOCTEST_SUBCASE("xml") + { + payload = R"( + + John + + fault + + + )"; + } + auto notif = ctx.parseOp(payload, dataTypeFor(payload), libyang::OperationType::NotificationYang); + REQUIRE(notif.tree); + REQUIRE(notif.op); + REQUIRE(notif.op != notif.tree); + REQUIRE(notif.tree->path() == "/example-schema:person[name='John']"); + auto node = notif.op->findPath("/example-schema:person[name='John']/event/description"); + REQUIRE(!!node); + REQUIRE(std::visit(libyang::ValuePrinter{}, node->asTerm().value()) == "fault"); + } + } } DOCTEST_SUBCASE("invalid notification") { @@ -2116,6 +2185,9 @@ TEST_CASE("Data Node manipulation") REQUIRE_THROWS_WITH_AS(ctx.parseOp("", libyang::DataFormat::XML, libyang::OperationType::NotificationNetconf), "Can't parse a standalone rpc/action/notification into operation data tree: LY_ENOT", libyang::Error); + REQUIRE_THROWS_WITH_AS(ctx.parseOp("asd", libyang::DataFormat::XML, libyang::OperationType::NotificationYang), + "Can't parse a standalone rpc/action/notification into operation data tree: LY_EVALID", libyang::Error); + /* libyang::setLogOptions(libyang::LogOptions::Log | libyang::LogOptions::Store); */ REQUIRE_THROWS_WITH_AS(ctx.parseOp(R"( { diff --git a/tests/example_schema.hpp b/tests/example_schema.hpp index c3892550..dc279493 100644 --- a/tests/example_schema.hpp +++ b/tests/example_schema.hpp @@ -100,6 +100,12 @@ module example-schema { leaf name { type string; } + + notification event { + leaf description { + type string; + } + } } leaf bossPerson { From 435219106c2d1dbf413e7bb6d0aba4c191671102 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= Date: Mon, 1 Jul 2024 12:07:44 +0200 Subject: [PATCH 13/57] CI: temporary pin of libyang to 3.0.18 There is ODR violation in libyang tests reported by ASan which breaks our CI builds. This commit temporarily (until this gets solved in upstream) pins the libyang version to 3.0.18 which builds just fine. The log from ASan: 52/60 Test #31: utest_plugins .....................***Failed 1.22 sec [==========] tests: Running 4 test(s). [ RUN ] test_add_invalid [ OK ] test_add_invalid [ RUN ] test_add_simple ================================================================= ==2826==ERROR: AddressSanitizer: odr-violation (0x7f6d170dcb40): [1] size=24 'ly_version_so' /home/ci/src/cesnet-gerrit-public/github/CESNET/libyang/src/ly_common.c:43 [2] size=24 'ly_version_so' /home/ci/src/cesnet-gerrit-public/github/CESNET/libyang/src/ly_common.c:43 These globals were registered at these points: [1]: #0 0x437ffa in __asan_register_globals (/home/ci/build/github/CESNET/libyang/tests/utest_plugins+0x437ffa) (BuildId: e79d14f37e59b6533b66d19363b25fea9e324edb) #1 0x7f6d16691b9e in asan.module_ctor ly_common.c #2 0x7f6d1aad7236 in call_init /usr/src/debug/glibc-2.37-18.fc38.x86_64/elf/dl-init.c:74:3 #3 0x7f6d1aad7236 in call_init /usr/src/debug/glibc-2.37-18.fc38.x86_64/elf/dl-init.c:26:1 #4 0x7f6d1aad732c in _dl_init /usr/src/debug/glibc-2.37-18.fc38.x86_64/elf/dl-init.c:121:5 #5 0x7f6d1aad35c1 in _dl_catch_exception /usr/src/debug/glibc-2.37-18.fc38.x86_64/elf/dl-catch.c:211:7 #6 0x7f6d1aaddeeb in dl_open_worker /usr/src/debug/glibc-2.37-18.fc38.x86_64/elf/dl-open.c:827:5 #7 0x7f6d1aad3522 in _dl_catch_exception /usr/src/debug/glibc-2.37-18.fc38.x86_64/elf/dl-catch.c:237:8 #8 0x7f6d1aade2e3 in _dl_open /usr/src/debug/glibc-2.37-18.fc38.x86_64/elf/dl-open.c:903:17 #9 0x7f6d1a7b5713 in dlopen_doit /usr/src/debug/glibc-2.37-18.fc38.x86_64/dlfcn/dlopen.c:56:15 #10 0x7f6d1aad3522 in _dl_catch_exception /usr/src/debug/glibc-2.37-18.fc38.x86_64/elf/dl-catch.c:237:8 #11 0x7f6d1aad3678 in _dl_catch_error /usr/src/debug/glibc-2.37-18.fc38.x86_64/elf/dl-catch.c:256:19 #12 0x7f6d1a7b51f2 in _dlerror_run /usr/src/debug/glibc-2.37-18.fc38.x86_64/dlfcn/dlerror.c:138:17 #13 0x7f6d1a7b57ce /usr/src/debug/glibc-2.37-18.fc38.x86_64/dlfcn/dlopen.c:71:10 #14 0x7f6d1a7b57ce in dlopen@@GLIBC_2.34 /usr/src/debug/glibc-2.37-18.fc38.x86_64/dlfcn/dlopen.c:81:12 #15 0x485f82 in dlopen (/home/ci/build/github/CESNET/libyang/tests/utest_plugins+0x485f82) (BuildId: e79d14f37e59b6533b66d19363b25fea9e324edb) #16 0xcb5ee3 in plugins_load_module /home/ci/src/cesnet-gerrit-public/github/CESNET/libyang/src/plugins.c:378:17 #17 0xcb5e67 in lyplg_add /home/ci/src/cesnet-gerrit-public/github/CESNET/libyang/src/plugins.c:589:11 #18 0xea2a60 in test_add_simple /home/ci/src/cesnet-gerrit-public/github/CESNET/libyang/tests/utests/basic/test_plugins.c:50:5 #19 0x7f6d1aac218f (/lib64/libcmocka.so.0+0x618f) (BuildId: 785844a0941c0bde763740a981d056f60aa9c7b7) #20 0x7f6d1aac2904 in _cmocka_run_group_tests (/lib64/libcmocka.so.0+0x6904) (BuildId: 785844a0941c0bde763740a981d056f60aa9c7b7) #21 0xea21a2 in main /home/ci/src/cesnet-gerrit-public/github/CESNET/libyang/tests/utests/basic/test_plugins.c:152:12 Change-Id: Id8a4771296d2e85de2ec29fbd17046d280d2eda2 --- .zuul.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.zuul.yaml b/.zuul.yaml index b41c4904..8271057a 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/2024-07-08--3.0.18 - 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/2024-07-08--3.0.18 - name: github/onqtam/doctest override-checkout: v2.4.11 - f38-clang-tsan: From c88f5ec04149b6744b6915cb8a20afe2dfe170e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= Date: Sun, 7 Jul 2024 13:43:42 +0200 Subject: [PATCH 14/57] add missing #include Discovered on MSVC. Change-Id: I29391ec31c598efc5e74adc398958b6f8d530bec --- include/libyang-cpp/Module.hpp | 1 + 1 file changed, 1 insertion(+) diff --git a/include/libyang-cpp/Module.hpp b/include/libyang-cpp/Module.hpp index 3d213eaa..4bc55735 100644 --- a/include/libyang-cpp/Module.hpp +++ b/include/libyang-cpp/Module.hpp @@ -9,6 +9,7 @@ #include #include +#include #include #include #include From d29365c99b55270159f1a56c0b9d2295131b6fee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= Date: Tue, 18 Jun 2024 16:48:19 +0200 Subject: [PATCH 15/57] add utility function for formatting time_points in yang:date-and-time Many of our projects implement conversion of a time_point into a yang:date-and-time string. It makes no sense to have this code scattered across our codebase, let us implement in in one place, in libyang-cpp. This is a header only implementation. The problem is that std::chrono stream implementations are C++20 feature but they might not be implemented in the STL (e.g., libstdc++ implements this in gcc version 13 and 14). The header tries to detect if your STL supports the needed std::chrono features and in case it does not it tries to include headers from the external HowardHinnant/date library[1] (you have to link your executable to the library yourself, there is no configuration for that). [1] https://github.com/HowardHinnant/date Change-Id: I51242fe2ec873fba5954c44e38e898d205a9e484 --- CMakeLists.txt | 15 +++++ include/libyang-cpp/Time.hpp | 124 +++++++++++++++++++++++++++++++++++ tests/time.cpp | 59 +++++++++++++++++ 3 files changed, 198 insertions(+) create mode 100644 include/libyang-cpp/Time.hpp create mode 100644 tests/time.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index d1003d25..08684660 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -31,6 +31,9 @@ find_package(PkgConfig REQUIRED) pkg_check_modules(LIBYANG REQUIRED libyang>=3.0.11 IMPORTED_TARGET) set(LIBYANG_CPP_PKG_VERSION "2") +# 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) + include_directories(${CMAKE_CURRENT_SOURCE_DIR}/include) include(CheckIncludeFileCXX) @@ -76,6 +79,7 @@ if(${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.24") libyang-cpp/Module.hpp libyang-cpp/Set.hpp libyang-cpp/SchemaNode.hpp + libyang-cpp/Time.hpp libyang-cpp/Type.hpp libyang-cpp/Utils.hpp libyang-cpp/Value.hpp @@ -109,6 +113,17 @@ if(BUILD_TESTING) libyang_cpp_test(schema_node) libyang_cpp_test(unsafe) target_link_libraries(test_unsafe PkgConfig::LIBYANG) + + if(date_FOUND) + add_executable(test_time-stl-hhdate tests/time.cpp) + target_link_libraries(test_time-stl-hhdate DoctestIntegration yang-cpp date::date-tz) + add_test(test_time-stl-hhdate test_time-stl-hhdate) + + add_executable(test_time-hhdate tests/time.cpp) + target_link_libraries(test_time-hhdate DoctestIntegration yang-cpp date::date-tz) + target_compile_definitions(test_time-hhdate PUBLIC LIBYANG_CPP_SKIP_STD_CHRONO_TZ) + add_test(test_time-hhdate test_time-hhdate) + endif() endif() if(WITH_DOCS) diff --git a/include/libyang-cpp/Time.hpp b/include/libyang-cpp/Time.hpp new file mode 100644 index 00000000..36921f10 --- /dev/null +++ b/include/libyang-cpp/Time.hpp @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2024 CESNET, https://photonics.cesnet.cz/ + * + * Written by Tomáš Pecka + * + * SPDX-License-Identifier: BSD-3-Clause + */ +#pragma once + +#include +#include "libyang-cpp/export.h" + +#if __cpp_lib_chrono >= 201907L && !LIBYANG_CPP_SKIP_STD_CHRONO_TZ +namespace libyang { +namespace date_impl = std::chrono; +} +#define LIBYANG_CPP_TIME_BACKEND_STL +#else +#include +namespace libyang { +namespace date_impl = date; +} +#define LIBYANG_CPP_TIME_BACKEND_HHDATE +#endif + +#if !defined(LIBYANG_CPP_TIME_BACKEND_STL) && !defined(LIBYANG_CPP_TIME_BACKEND_HHDATE) +#error "Neither compatible STL backend nor HowardHinnant/date found" +#endif + +namespace libyang { +using namespace std::string_literals; + +#define LIBYANG_CPP_TIME_FORMAT_DATETIME_BASE "%Y-%m-%dT%H:%M:%S" +#define LIBYANG_CPP_TIME_FORMAT_TZ LIBYANG_CPP_TIME_FORMAT_DATETIME_BASE "%Ez" +#define LIBYANG_CPP_TIME_FORMAT_NO_TZ LIBYANG_CPP_TIME_FORMAT_DATETIME_BASE "-00:00" +#define LIBYANG_CPP_TIME_FORMAT_UTC LIBYANG_CPP_TIME_FORMAT_DATETIME_BASE "+00:00" + +/** @brief Interprets the time point in special timezones */ +enum class TimezoneInterpretation { + Unspecified, //* the timezone of the time point is unspecified */ + Local, //* interprets the time point as if local timezone */ +}; + +#ifdef LIBYANG_CPP_TIME_BACKEND_STL +/** @brief Converts a time point of local time to a string representing yang:date-and-time with unspecified TZ. */ +template +LIBYANG_CPP_EXPORT std::string yangTimeFormat(const std::chrono::local_time& timePoint) +{ + return std::format("{:" LIBYANG_CPP_TIME_FORMAT_NO_TZ "}", timePoint); +} +#endif + +#ifdef TZ_H +/** @brief Converts a date::local_time to a string representing yang:date-and-time with unspecified TZ. */ +template +LIBYANG_CPP_EXPORT std::string yangTimeFormat(const date::local_time& timePoint) +{ + return date::format(LIBYANG_CPP_TIME_FORMAT_NO_TZ, timePoint); +} +#endif + +#ifdef LIBYANG_CPP_TIME_BACKEND_STL +/** @brief Converts a time point of a time with timezone to a string representing yang:date-and-time. */ +template +LIBYANG_CPP_EXPORT std::string yangTimeFormat(const std::chrono::zoned_time& zonedTime) +{ + return std::format("{:" LIBYANG_CPP_TIME_FORMAT_TZ "}", zonedTime); +} +#endif + +#ifdef TZ_H +/** @brief Converts a time point of a time with timezone (from date library) to a string representing yang:date-and-time. */ +template +LIBYANG_CPP_EXPORT std::string yangTimeFormat(const date::zoned_time& zonedTime) +{ + return date::format(LIBYANG_CPP_TIME_FORMAT_TZ, zonedTime); +} +#endif + +template +/** @brief Converts a system_clock time to a string representing yang:date-and-time. */ +LIBYANG_CPP_EXPORT std::string yangTimeFormat(const std::chrono::time_point& timePoint, TimezoneInterpretation tz) +{ + switch (tz) { + case TimezoneInterpretation::Unspecified: +#if defined(LIBYANG_CPP_TIME_BACKEND_STL) + return std::format("{:" LIBYANG_CPP_TIME_FORMAT_NO_TZ "}", timePoint); +#elif defined(LIBYANG_CPP_TIME_BACKEND_HHDATE) + return date::format(LIBYANG_CPP_TIME_FORMAT_NO_TZ, timePoint); +#endif + case TimezoneInterpretation::Local: + const auto* tzdata = date_impl::current_zone(); + return yangTimeFormat(date_impl::zoned_time{tzdata, timePoint}); + } + + __builtin_unreachable(); +} + +/** @brief Converts a utc_clock time to a string representing yang:date-and-time. */ +template +LIBYANG_CPP_EXPORT std::string yangTimeFormat(const std::chrono::time_point& timePoint) +{ +#if defined(LIBYANG_CPP_TIME_BACKEND_STL) + return std::format("{:" LIBYANG_CPP_TIME_FORMAT_UTC "}", timePoint); +#elif defined(LIBYANG_CPP_TIME_BACKEND_HHDATE) + return date::format(LIBYANG_CPP_TIME_FORMAT_UTC, std::chrono::clock_cast(timePoint)); +#endif +} + +/** @brief Converts a textual representation yang:date-and-time to std::time_point */ +template +LIBYANG_CPP_EXPORT std::chrono::time_point fromYangTimeFormat(const std::string& timeStr) +{ + std::chrono::time_point timePoint; + std::istringstream iss(timeStr); + + if (!(iss >> date_impl::parse(LIBYANG_CPP_TIME_FORMAT_TZ, timePoint))) { + throw std::invalid_argument("Invalid date for format string '"s + LIBYANG_CPP_TIME_FORMAT_TZ + "'"); + } + + return timePoint; +} + +} diff --git a/tests/time.cpp b/tests/time.cpp new file mode 100644 index 00000000..d6c332c1 --- /dev/null +++ b/tests/time.cpp @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2024 CESNET, https://photonics.cesnet.cz/ + * + * Written by Tomáš Pecka + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +#include +#include +#include + +TEST_CASE("Time utils") +{ + using libyang::fromYangTimeFormat; + using libyang::TimezoneInterpretation; + using libyang::yangTimeFormat; + + using namespace std::chrono_literals; + + DOCTEST_SUBCASE("STL structures") + { + using namespace std::chrono; + + const auto sys_time = std::chrono::sys_days{year(2021) / January / day(23)} + 06h + 5min + 23s + 20ms; + REQUIRE(yangTimeFormat(sys_time, TimezoneInterpretation::Unspecified) == "2021-01-23T06:05:23.020-00:00"); + + const auto utc_time = std::chrono::clock_cast(sys_time); + REQUIRE(yangTimeFormat(utc_time) == "2021-01-23T06:05:23.020+00:00"); + + REQUIRE(fromYangTimeFormat("2021-01-23T06:05:23.020-00:00") == sys_time); + } + + DOCTEST_SUBCASE("HowardHinnant/date") + { + using namespace date::literals; + + const auto loc_time = date::local_days{2021_y / date::January / 23} + 06h + 5min + 23s + 20ms; + REQUIRE(yangTimeFormat(loc_time) == "2021-01-23T06:05:23.020-00:00"); + REQUIRE(yangTimeFormat(date::make_zoned(date::locate_zone("Europe/Prague"), loc_time)) == "2021-01-23T06:05:23.020+01:00"); + } + +#if __cpp_lib_chrono >= 201907L && !LIBYANG_CPP_SKIP_STD_CHRONO_TZ + DOCTEST_SUBCASE("Only C++20 with calendar and tz support") + { + using namespace std::chrono; + + const auto loc_time = std::chrono::local_days{year(2021) / June / day(23)} + 06h + 5min + 23s + 20ms; + REQUIRE(yangTimeFormat(std::chrono::zoned_time{"Europe/Prague", loc_time}) == "2021-06-23T06:05:23.020+02:00"); + REQUIRE(yangTimeFormat(std::chrono::zoned_time{"Australia/Eucla", loc_time}) == "2021-06-23T06:05:23.020+08:45"); + REQUIRE(yangTimeFormat(loc_time) == "2021-06-23T06:05:23.020-00:00"); + REQUIRE(fromYangTimeFormat("2021-06-23T06:05:23.020-00:00") == loc_time); + + const auto sys_time = std::chrono::sys_days{year(2021) / January / day(23)} + 06h + 5min + 23s + 20ms; + const auto utc_time = std::chrono::clock_cast(sys_time); + REQUIRE(fromYangTimeFormat("2021-01-23T06:05:23.020+00:00") == utc_time); + } +#endif +} From 08dc7ffa4525fff504fa99a275829ddca4cc08b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= Date: Tue, 16 Jul 2024 12:15:28 +0200 Subject: [PATCH 16/57] Revert "CI: temporary pin of libyang to 3.0.18" Fixed upstream. This reverts commit 435219106c2d1dbf413e7bb6d0aba4c191671102. Change-Id: I70586e2776492220298ab8f11aee71e16f3ef5fc --- .zuul.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.zuul.yaml b/.zuul.yaml index 8271057a..b41c4904 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -4,13 +4,13 @@ - f38-gcc-cover: required-projects: - name: github/CESNET/libyang - override-checkout: cesnet/2024-07-08--3.0.18 + 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/2024-07-08--3.0.18 + override-checkout: devel - name: github/onqtam/doctest override-checkout: v2.4.11 - f38-clang-tsan: From b7a3ccafe4d0d79c5da333bb977a91fcad58c556 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= Date: Tue, 23 Jul 2024 16:47:33 +0200 Subject: [PATCH 17/57] wrap extension data parsing Wrapper for lyd_parse_ext_data. Change-Id: I410048fb3ffddc16fb6a28a3cead2438c60c0a69 --- include/libyang-cpp/Context.hpp | 6 ++ src/Context.cpp | 33 +++++++++ tests/context.cpp | 126 ++++++++++++++++++++++++++++++++ 3 files changed, 165 insertions(+) diff --git a/include/libyang-cpp/Context.hpp b/include/libyang-cpp/Context.hpp index 6f6e2330..f0b93040 100644 --- a/include/libyang-cpp/Context.hpp +++ b/include/libyang-cpp/Context.hpp @@ -93,6 +93,12 @@ class LIBYANG_CPP_EXPORT Context { const DataFormat format, const std::optional parseOpts = std::nullopt, const std::optional validationOpts = std::nullopt) const; + std::optional parseExtData( + const ExtensionInstance& ext, + const std::string& data, + const DataFormat format, + const std::optional parseOpts = std::nullopt, + const std::optional validationOpts = std::nullopt) const; Module loadModule(const std::string& name, const std::optional& revision = std::nullopt, const std::vector& = {}) const; void setSearchDir(const std::filesystem::path& searchDir) const; std::optional getModule(const std::string& name, const std::optional& revision) const; diff --git a/src/Context.cpp b/src/Context.cpp index f49b2091..1c281cab 100644 --- a/src/Context.cpp +++ b/src/Context.cpp @@ -180,6 +180,39 @@ std::optional Context::parseData( return DataNode{tree, m_ctx}; } +/** + * @brief Parses data from a string representing extension data tree node. + * + * Wraps `lyd_parse_ext_data`. + */ +std::optional Context::parseExtData( + const ExtensionInstance& ext, + const std::string& data, + const DataFormat format, + const std::optional parseOpts, + const std::optional validationOpts) const +{ + auto in = wrap_ly_in_new_memory(data); + + lyd_node* tree = nullptr; + auto err = lyd_parse_ext_data( + ext.m_ext, + nullptr, + in.get(), + utils::toLydFormat(format), + parseOpts ? utils::toParseOptions(*parseOpts) : 0, + validationOpts ? utils::toValidationOptions(*validationOpts) : 0, + &tree); + throwIfError(err, "Can't parse extension data"); + + if (!tree) { + return std::nullopt; + } + + return DataNode{tree, m_ctx}; +} + + /** * @brief Parses YANG data into an operation data tree. * diff --git a/tests/context.cpp b/tests/context.cpp index f5641b44..8505b1ed 100644 --- a/tests/context.cpp +++ b/tests/context.cpp @@ -453,6 +453,132 @@ TEST_CASE("context") REQUIRE(data->findPath("/example-schema:leafInt8")->asTerm().valueStr() == "-43"); } + DOCTEST_SUBCASE("Context::parseExt") + { + ctx->setSearchDir(TESTS_DIR / "yang"); + + DOCTEST_SUBCASE("ietf-restconf") + { + auto mod = ctx->loadModule("ietf-restconf", "2017-01-26"); + auto ext = mod.extensionInstance("yang-errors"); + + auto node = ctx->parseExtData(ext, R"({"ietf-restconf:errors": {"error": [{"error-type": "protocol", "error-tag": "invalid-attribute", "error-message": "hi"}]}})", libyang::DataFormat::JSON); + REQUIRE(node); + + auto errorsNode = node->findXPath("/ietf-restconf:errors"); + REQUIRE(errorsNode.size() == 1); + REQUIRE(errorsNode.begin()->path() == "/ietf-restconf:errors"); + REQUIRE(*errorsNode.begin()->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::WithSiblings | libyang::PrintFlags::KeepEmptyCont) == R"({ + "ietf-restconf:errors": { + "error": [ + { + "error-type": "protocol", + "error-tag": "invalid-attribute", + "error-message": "hi" + } + ] + }, + "ietf-yang-schema-mount:schema-mounts": {} +} +)"); + } + + DOCTEST_SUBCASE("ietf-yang-patch") + { + auto mod = ctx->parseModule(example_schema, libyang::SchemaFormat::YANG); + auto ext = ctx->loadModule("ietf-yang-patch", "2017-02-22").extensionInstance("yang-patch"); + + std::string data; + libyang::DataFormat dataFormat; + + DOCTEST_SUBCASE("XML") + { + dataFormat = libyang::DataFormat::XML; + data = R"( + + add-songs-patch + + edit1 + create + /person=John + + + John + + + + + edit2 + create + /dummy + + I am a dummy + + + +)"; + } + DOCTEST_SUBCASE("JSON") + { + dataFormat = libyang::DataFormat::JSON; + data = R"({ + "ietf-yang-patch:yang-patch": { + "patch-id": "add-songs-patch", + "edit": [ + { + "edit-id": "edit1", + "operation": "create", + "target": "/person=John", + "value": { + "example-schema:person": { + "name": "John" + } + } + }, + { + "edit-id": "edit2", + "operation": "create", + "target": "/dummy", + "value": { + "example-schema:dummy": "I am a dummy" + } + } + ] + } +} +)"; + } + + auto node = ctx->parseExtData(ext, data, dataFormat); + REQUIRE(node); + auto edits = node->findXPath("/ietf-yang-patch:yang-patch/edit"); + REQUIRE(edits.size() == 2); + + auto firstValue = edits.begin()->findPath("value"); + REQUIRE(firstValue); + REQUIRE(*firstValue->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::KeepEmptyCont) == R"({ + "ietf-yang-patch:value": { + "example-schema:person": { + "name": "John" + } + } +} +)"); + REQUIRE(*firstValue->printStr(libyang::DataFormat::XML, libyang::PrintFlags::KeepEmptyCont) == R"( + + John + + +)"); + + auto secondValueNode = (edits.begin() + 1)->findPath("value"); + REQUIRE(secondValueNode); + auto secondValue = std::get(secondValueNode->asAny().releaseValue().value()); + REQUIRE(*secondValue.printStr(libyang::DataFormat::JSON, libyang::PrintFlags::KeepEmptyCont) == "{\n \"example-schema:dummy\": \"I am a dummy\"\n}\n"); + REQUIRE(*secondValue.printStr(libyang::DataFormat::XML, libyang::PrintFlags::KeepEmptyCont) == "I am a dummy\n"); + } + } + DOCTEST_SUBCASE("Log level") { REQUIRE(libyang::setLogLevel(libyang::LogLevel::Error) == libyang::LogLevel::Debug); From ff90d869c07f4bcb31d7b9c904a983c339d243d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= Date: Mon, 29 Jul 2024 16:56:45 +0200 Subject: [PATCH 18/57] docs: mention Doxygen and Doctest Thanks to Martin Maurer for reporting. Bug: https://github.com/CESNET/libyang-cpp/issues/25 Change-Id: Ia60086b1e356a85190851845cc00fb504bfcf204 --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 9009a540..d76975bc 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,8 @@ 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 the docs, Doxygen ## Building *libyang-cpp* uses *CMake* for building. From 38e3399c99a82d3c0f693fb19ab1e1b3cd72aed9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= Date: Thu, 15 Aug 2024 21:26:24 +0200 Subject: [PATCH 19/57] list rpc nodes of a module This patch adds a method that returns all RPC nodes of a module as a collection of libyang::SchemaNode. This might be useful for instance, in RESTCONF server if one wants to print all RPC nodes [1]. [1] https://datatracker.ietf.org/doc/html/rfc8040#section-3.3.2 Change-Id: Id0cf5c0574c5dfc1ad5bc9b93bee4f0235ddf55a --- include/libyang-cpp/Module.hpp | 1 + include/libyang-cpp/SchemaNode.hpp | 1 + src/Module.cpp | 18 ++++++++++++++++++ tests/context.cpp | 11 +++++++++++ tests/example_schema.hpp | 2 ++ 5 files changed, 33 insertions(+) diff --git a/include/libyang-cpp/Module.hpp b/include/libyang-cpp/Module.hpp index 4bc55735..e7d017f8 100644 --- a/include/libyang-cpp/Module.hpp +++ b/include/libyang-cpp/Module.hpp @@ -87,6 +87,7 @@ class LIBYANG_CPP_EXPORT Module { std::vector identities() const; ChildInstanstiables childInstantiables() const; + std::vector actionRpcs() const; std::string printStr(const SchemaOutputFormat format, const std::optional flags = std::nullopt, std::optional lineLength = std::nullopt) const; diff --git a/include/libyang-cpp/SchemaNode.hpp b/include/libyang-cpp/SchemaNode.hpp index bb447ccb..63a48a54 100644 --- a/include/libyang-cpp/SchemaNode.hpp +++ b/include/libyang-cpp/SchemaNode.hpp @@ -82,6 +82,7 @@ class LIBYANG_CPP_EXPORT SchemaNode { friend Context; friend DataNode; friend List; + friend Module; friend ChildInstanstiablesIterator; friend Iterator; friend Iterator; diff --git a/src/Module.cpp b/src/Module.cpp index edbeb339..5e3aba47 100644 --- a/src/Module.cpp +++ b/src/Module.cpp @@ -191,6 +191,24 @@ ChildInstanstiables Module::childInstantiables() const return ChildInstanstiables{nullptr, m_module->compiled, m_ctx}; } +/** + * @brief Returns a collection of RPC nodes (not action nodes) as SchemaNode + * + * Wraps `lys_module::compiled::rpc`. + */ +std::vector Module::actionRpcs() const +{ + if (!m_module->compiled) { + throw Error{"Module \"" + this->name() + "\" not implemented"}; + } + + std::vector res; + for (auto node = m_module->compiled->rpcs; node; node = node->next) { + res.emplace_back(SchemaNode(&node->node, m_ctx)); + } + return res; +} + std::vector Module::extensionInstances() const { if (!m_module->compiled) { diff --git a/tests/context.cpp b/tests/context.cpp index 8505b1ed..74a79b95 100644 --- a/tests/context.cpp +++ b/tests/context.cpp @@ -150,6 +150,17 @@ TEST_CASE("context") REQUIRE_THROWS_WITH_AS(modRestconf->extensionInstance("yay"), "Extension \"yay\" not defined in module \"ietf-restconf\"", libyang::Error); } + DOCTEST_SUBCASE("Module RPC nodes") + { + auto mod = ctx->parseModule(example_schema, libyang::SchemaFormat::YANG); + auto rpcs = mod.actionRpcs(); + REQUIRE(rpcs.size() == 1); + REQUIRE(rpcs[0].module().name() == "example-schema"); + REQUIRE(rpcs[0].name() == "myRpc"); + + REQUIRE(ctx->parseModule(example_schema2, libyang::SchemaFormat::YANG).actionRpcs().empty()); + } + DOCTEST_SUBCASE("context lifetime") { ctx->parseModule(valid_yang_model, libyang::SchemaFormat::YANG); diff --git a/tests/example_schema.hpp b/tests/example_schema.hpp index dc279493..2408f76c 100644 --- a/tests/example_schema.hpp +++ b/tests/example_schema.hpp @@ -106,6 +106,8 @@ module example-schema { type string; } } + + action poke { } } leaf bossPerson { From 8d899938372b25da8e53d8261e53fce4956c06f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= Date: Tue, 17 Sep 2024 10:39:54 +0200 Subject: [PATCH 20/57] Bump the minimal required libyang ...so that we require what we're testing against. Downstream users, if you revert this, note that you're on your own; we're only interested in supporting the latest & greatest. Change-Id: I439198af708110175f4c033b1e3afadade5ae629 --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 08684660..94ac7168 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.0.11 IMPORTED_TARGET) +pkg_check_modules(LIBYANG REQUIRED libyang>=3.4.2 IMPORTED_TARGET) set(LIBYANG_CPP_PKG_VERSION "2") # FIXME from gcc 14.1 on we should be able to use the calendar/time from libstdc++ and thus remove the date dependency From de3fc82325708ecc0ae09ee2d5d9bb81c2b54805 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= Date: Tue, 17 Sep 2024 10:45:43 +0200 Subject: [PATCH 21/57] API/ABI change: stop using string_view Change-Id: I858d70b67308e40b528b71bbd409aae7ba279b25 --- include/libyang-cpp/Context.hpp | 8 ++++---- tests/context.cpp | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/include/libyang-cpp/Context.hpp b/include/libyang-cpp/Context.hpp index f0b93040..6e0735de 100644 --- a/include/libyang-cpp/Context.hpp +++ b/include/libyang-cpp/Context.hpp @@ -53,10 +53,10 @@ struct LIBYANG_CPP_EXPORT ModuleInfo { * @param submodName Optional missing submodule name. std::nullopt if requesting the main module * @param submodRev Optional missing submodule revision. std::nullopt if requesting the latest submodule revision. */ -using ModuleCallback = std::optional(std::string_view modName, - std::optional modRevision, - std::optional submodName, - std::optional submodRev); +using ModuleCallback = std::optional(const std::string& modName, + const std::optional& modRevision, + const std::optional& submodName, + const std::optional& submodRev); /** * @brief Contains detailed libyang error. diff --git a/tests/context.cpp b/tests/context.cpp index 74a79b95..71ae8738 100644 --- a/tests/context.cpp +++ b/tests/context.cpp @@ -402,7 +402,7 @@ TEST_CASE("context") DOCTEST_SUBCASE("Context::registerModuleCallback") { auto numCalled = 0; - ctx->registerModuleCallback([&numCalled](std::string_view modName, auto, auto, auto) -> std::optional { + ctx->registerModuleCallback([&numCalled](auto modName, auto, auto, auto) -> std::optional { numCalled++; if (modName == "example-schema") { return libyang::ModuleInfo{ @@ -421,7 +421,7 @@ TEST_CASE("context") DOCTEST_SUBCASE("Implemented modules") { - ctx->registerModuleCallback([](std::string_view modName, auto, auto, auto) -> std::optional { + ctx->registerModuleCallback([](auto modName, auto, auto, auto) -> std::optional { if (modName == "withImport") { return libyang::ModuleInfo{ .data = model_with_import, From 3af3da8e1b17c7bc0ee38685b6f0d3a865fec16d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= Date: Mon, 23 Sep 2024 17:02:44 +0200 Subject: [PATCH 22/57] docs: clarify how extensions work Fixes: fb60895 Allow getting module extensions Change-Id: I49a69608243c4ceb95736ed66c4ce891ba106815 --- include/libyang-cpp/Module.hpp | 2 +- src/Module.cpp | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/include/libyang-cpp/Module.hpp b/include/libyang-cpp/Module.hpp index e7d017f8..2929258c 100644 --- a/include/libyang-cpp/Module.hpp +++ b/include/libyang-cpp/Module.hpp @@ -156,7 +156,7 @@ class LIBYANG_CPP_EXPORT Identity { /** * @brief Contains information about compiled extension. * - * Wraps `lysc_extension_instance` + * Wraps `lysc_ext_instance` */ class LIBYANG_CPP_EXPORT ExtensionInstance { public: diff --git a/src/Module.cpp b/src/Module.cpp index 5e3aba47..8a424b2d 100644 --- a/src/Module.cpp +++ b/src/Module.cpp @@ -391,7 +391,11 @@ ExtensionInstance::ExtensionInstance(const lysc_ext_instance* ext, std::shared_p } /** - * @brief Returns the argument + * @brief Returns the argument name + * + * As an example, the RESTCONF RFC defines an extension named "yang-data". + * This extension is then instantiated at two places by that RFC, under + * names "yang-errors" and "yang-api". * * Wraps `lysc_ext_instance::argument`. */ @@ -417,7 +421,7 @@ Extension::Extension(const lysc_ext* ext, std::shared_ptr ctx) } /** - * @brief Returns the name of the module. + * @brief Returns the name of the extension definition * * Wraps `lysc_ext::name`. */ From c1342a25f5ad279df3c46d3324830b523140db44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= Date: Mon, 23 Sep 2024 17:39:01 +0200 Subject: [PATCH 23/57] refactor: ExtensionInstance is about instantiated extensions ...so it's a tad confusing to call that private member `m_ext` which is already used in the extension definition. Change-Id: I744eabf8c4e18c68c630357a6580af9df801d20b --- include/libyang-cpp/Module.hpp | 4 ++-- src/Context.cpp | 4 ++-- src/DataNode.cpp | 2 +- src/Module.cpp | 8 ++++---- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/include/libyang-cpp/Module.hpp b/include/libyang-cpp/Module.hpp index 2929258c..41c180a3 100644 --- a/include/libyang-cpp/Module.hpp +++ b/include/libyang-cpp/Module.hpp @@ -164,9 +164,9 @@ class LIBYANG_CPP_EXPORT ExtensionInstance { std::string argument() const; private: - ExtensionInstance(const lysc_ext_instance* ext, std::shared_ptr ctx); + ExtensionInstance(const lysc_ext_instance* instance, std::shared_ptr ctx); - const lysc_ext_instance* m_ext; + const lysc_ext_instance* m_instance; std::shared_ptr m_ctx; friend Module; diff --git a/src/Context.cpp b/src/Context.cpp index 1c281cab..821106db 100644 --- a/src/Context.cpp +++ b/src/Context.cpp @@ -196,7 +196,7 @@ std::optional Context::parseExtData( lyd_node* tree = nullptr; auto err = lyd_parse_ext_data( - ext.m_ext, + ext.m_instance, nullptr, in.get(), utils::toLydFormat(format), @@ -362,7 +362,7 @@ CreatedNodes Context::newPath2(const std::string& path, libyang::JSON json, cons */ std::optional Context::newExtPath(const std::string& path, const std::optional& value, const ExtensionInstance& ext, const std::optional options) const { - auto out = impl::newExtPath(nullptr, ext.m_ext, std::make_shared(m_ctx), path, value, options); + auto out = impl::newExtPath(nullptr, ext.m_instance, std::make_shared(m_ctx), path, value, options); if (!out) { throw std::logic_error("Expected a new node to be created"); diff --git a/src/DataNode.cpp b/src/DataNode.cpp index f7a2772e..609c3506 100644 --- a/src/DataNode.cpp +++ b/src/DataNode.cpp @@ -324,7 +324,7 @@ CreatedNodes DataNode::newPath2(const std::string& path, libyang::XML xml, const */ std::optional DataNode::newExtPath(const std::string& path, const std::optional& value, const ExtensionInstance& ext, const std::optional options) const { - auto out = impl::newExtPath(m_node, ext.m_ext, nullptr, path, value, options); + auto out = impl::newExtPath(m_node, ext.m_instance, nullptr, path, value, options); if (!out) { throw std::logic_error("Expected a new node to be created"); diff --git a/src/Module.cpp b/src/Module.cpp index 8a424b2d..79faa5c8 100644 --- a/src/Module.cpp +++ b/src/Module.cpp @@ -384,8 +384,8 @@ bool Identity::operator==(const Identity& other) const return module().name() == other.module().name() && name() == other.name(); } -ExtensionInstance::ExtensionInstance(const lysc_ext_instance* ext, std::shared_ptr ctx) - : m_ext(ext) +ExtensionInstance::ExtensionInstance(const lysc_ext_instance* instance, std::shared_ptr ctx) + : m_instance(instance) , m_ctx(ctx) { } @@ -401,7 +401,7 @@ ExtensionInstance::ExtensionInstance(const lysc_ext_instance* ext, std::shared_p */ std::string ExtensionInstance::argument() const { - return m_ext->argument; + return m_instance->argument; } /** @@ -411,7 +411,7 @@ std::string ExtensionInstance::argument() const */ Extension ExtensionInstance::definition() const { - return Extension{m_ext->def, m_ctx}; + return Extension{m_instance->def, m_ctx}; } Extension::Extension(const lysc_ext* ext, std::shared_ptr ctx) From 0fd169cafeac364a7cb1b2200d4ec1ec7a1b03a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= Date: Mon, 23 Sep 2024 17:43:57 +0200 Subject: [PATCH 24/57] API/ABI change: creating paths from extensions IMHO it makes a bit more sense to have the ExtensionInstance as the very first argument; the code looks more readable to me that way. Change-Id: I96eedb0c879a1932e7fd2c4337c980105efece4c --- include/libyang-cpp/Context.hpp | 2 +- include/libyang-cpp/DataNode.hpp | 2 +- src/Context.cpp | 4 ++-- src/DataNode.cpp | 4 ++-- tests/data_node.cpp | 8 ++++---- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/include/libyang-cpp/Context.hpp b/include/libyang-cpp/Context.hpp index 6e0735de..baa47b30 100644 --- a/include/libyang-cpp/Context.hpp +++ b/include/libyang-cpp/Context.hpp @@ -114,7 +114,7 @@ class LIBYANG_CPP_EXPORT Context { CreatedNodes newPath2(const std::string& path, const std::optional& value = std::nullopt, const std::optional options = std::nullopt) const; CreatedNodes newPath2(const std::string& path, libyang::JSON json, const std::optional options = std::nullopt) const; CreatedNodes newPath2(const std::string& path, libyang::XML xml, const std::optional options = std::nullopt) const; - std::optional newExtPath(const std::string& path, const std::optional& value, const ExtensionInstance& ext, const std::optional options = std::nullopt) const; + std::optional newExtPath(const ExtensionInstance& ext, const std::string& path, const std::optional& value, const std::optional options = std::nullopt) const; std::optional newOpaqueJSON(const std::string& moduleName, const std::string& name, const std::optional& value) const; std::optional newOpaqueXML(const std::string& moduleName, const std::string& name, const std::optional& value) const; SchemaNode findPath(const std::string& dataPath, const InputOutputNodes inputOutputNodes = InputOutputNodes::Input) const; diff --git a/include/libyang-cpp/DataNode.hpp b/include/libyang-cpp/DataNode.hpp index 839c33f5..22114155 100644 --- a/include/libyang-cpp/DataNode.hpp +++ b/include/libyang-cpp/DataNode.hpp @@ -94,7 +94,7 @@ class LIBYANG_CPP_EXPORT DataNode { CreatedNodes newPath2(const std::string& path, const std::optional& value = std::nullopt, const std::optional options = std::nullopt) const; CreatedNodes newPath2(const std::string& path, libyang::JSON json, const std::optional options = std::nullopt) const; CreatedNodes newPath2(const std::string& path, libyang::XML xml, const std::optional options = std::nullopt) const; - std::optional newExtPath(const std::string& path, const std::optional& value, const ExtensionInstance& ext, const std::optional options = std::nullopt) const; + std::optional newExtPath(const ExtensionInstance& ext, const std::string& path, const std::optional& value, const std::optional options = std::nullopt) const; void newMeta(const Module& module, const std::string& name, const std::string& value); MetaCollection meta() const; diff --git a/src/Context.cpp b/src/Context.cpp index 821106db..b844bd91 100644 --- a/src/Context.cpp +++ b/src/Context.cpp @@ -354,13 +354,13 @@ CreatedNodes Context::newPath2(const std::string& path, libyang::JSON json, cons /** * @brief Creates a new extension node with the supplied path, creating a completely new tree. * + * @param ext Extension instance where the node being created is defined. * @param path Path of the new node. * @param value String representation of the value. Use std::nullopt for non-leaf nodes and the `empty` type. - * @param ext Extension instance where the node being created is defined. * @param options Options that change the behavior of this method. * @return Returns the first created parent. */ -std::optional Context::newExtPath(const std::string& path, const std::optional& value, const ExtensionInstance& ext, const std::optional options) const +std::optional Context::newExtPath(const ExtensionInstance& ext, const std::string& path, const std::optional& value, const std::optional options) const { auto out = impl::newExtPath(nullptr, ext.m_instance, std::make_shared(m_ctx), path, value, options); diff --git a/src/DataNode.cpp b/src/DataNode.cpp index 609c3506..51c86c8b 100644 --- a/src/DataNode.cpp +++ b/src/DataNode.cpp @@ -316,13 +316,13 @@ CreatedNodes DataNode::newPath2(const std::string& path, libyang::XML xml, const /** * @brief Creates a new extension node with the supplied path, changing this tree. * + * @param ext Extension instance where the node being created is defined. * @param path Path of the new node. * @param value String representation of the value. Use std::nullopt for non-leaf nodes and the `empty` type. - * @param ext Extension instance where the node being created is defined. * @param options Options that change the behavior of this method. * @return Returns the first created parent. */ -std::optional DataNode::newExtPath(const std::string& path, const std::optional& value, const ExtensionInstance& ext, const std::optional options) const +std::optional DataNode::newExtPath(const ExtensionInstance& ext, const std::string& path, const std::optional& value, const std::optional options) const { auto out = impl::newExtPath(m_node, ext.m_instance, nullptr, path, value, options); diff --git a/tests/data_node.cpp b/tests/data_node.cpp index 812eb27f..a23e4c2a 100644 --- a/tests/data_node.cpp +++ b/tests/data_node.cpp @@ -2007,7 +2007,7 @@ TEST_CASE("Data Node manipulation") auto mod = ctx.loadModule("ietf-restconf", "2017-01-26"); auto ext = mod.extensionInstance("yang-errors"); - auto node = ctx.newExtPath("/ietf-restconf:errors", std::nullopt, ext, std::nullopt); + auto node = ctx.newExtPath(ext, "/ietf-restconf:errors", std::nullopt, std::nullopt); REQUIRE(node); REQUIRE(node->schema().name() == "errors"); REQUIRE(*node->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::WithSiblings | libyang::PrintFlags::KeepEmptyCont) == R"({ @@ -2017,7 +2017,7 @@ TEST_CASE("Data Node manipulation") REQUIRE(node->newPath("ietf-restconf:error[1]/error-type", "protocol")); REQUIRE(node->newPath("ietf-restconf:error[1]/error-tag", "invalid-attribute")); - REQUIRE(node->newExtPath("/ietf-restconf:errors/error[1]/error-message", "ahoj", ext)); + REQUIRE(node->newExtPath(ext, "/ietf-restconf:errors/error[1]/error-message", "ahoj")); REQUIRE_THROWS_WITH(node->newPath("ietf-restconf:error[1]/error-message", "duplicate create"), "Couldn't create a node with path 'ietf-restconf:error[1]/error-message': LY_EEXIST"); REQUIRE(*node->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::WithSiblings | libyang::PrintFlags::KeepEmptyCont) == R"({ "ietf-restconf:errors": { @@ -2032,8 +2032,8 @@ TEST_CASE("Data Node manipulation") } )"); - REQUIRE(node->newExtPath("/ietf-restconf:errors/error[2]/error-type", "transport", ext)); - REQUIRE(node->newExtPath("/ietf-restconf:errors/error[2]/error-tag", "invalid-attribute", ext)); + REQUIRE(node->newExtPath(ext, "/ietf-restconf:errors/error[2]/error-type", "transport")); + REQUIRE(node->newExtPath(ext, "/ietf-restconf:errors/error[2]/error-tag", "invalid-attribute")); REQUIRE(node->newPath("ietf-restconf:error[2]/error-message", "aaa")); REQUIRE(*node->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::WithSiblings | libyang::PrintFlags::KeepEmptyCont) == R"({ "ietf-restconf:errors": { From 4857ac943bce930ba51af4c174c7637d2e63cf0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= Date: Mon, 23 Sep 2024 18:23:15 +0200 Subject: [PATCH 25/57] allow retrieval of a schema node's extension instances MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This was inspired by a patch from Bedrich (Ib0dfb68afaf0f74d3defe87b16b7c34457cc4899). I wanted a little more incremental approach. Co-authored-by: Bedřich Schindler Change-Id: Ic767dd4da4b194fe40fe2ede01e51fbc77bd3bb1 --- include/libyang-cpp/Module.hpp | 1 + include/libyang-cpp/SchemaNode.hpp | 1 + src/SchemaNode.cpp | 14 ++++++++++++++ tests/example_schema.hpp | 21 +++++++++++++++++++++ tests/schema_node.cpp | 11 +++++++++++ 5 files changed, 48 insertions(+) diff --git a/include/libyang-cpp/Module.hpp b/include/libyang-cpp/Module.hpp index 41c180a3..a3fb7e13 100644 --- a/include/libyang-cpp/Module.hpp +++ b/include/libyang-cpp/Module.hpp @@ -172,6 +172,7 @@ class LIBYANG_CPP_EXPORT ExtensionInstance { friend Module; friend Context; friend DataNode; + friend SchemaNode; }; /** diff --git a/include/libyang-cpp/SchemaNode.hpp b/include/libyang-cpp/SchemaNode.hpp index 63a48a54..83afc067 100644 --- a/include/libyang-cpp/SchemaNode.hpp +++ b/include/libyang-cpp/SchemaNode.hpp @@ -74,6 +74,7 @@ class LIBYANG_CPP_EXPORT SchemaNode { Collection childrenDfs() const; Collection siblings() const; Collection immediateChildren() const; + std::vector extensionInstances() const; std::vector when() const; diff --git a/src/SchemaNode.cpp b/src/SchemaNode.cpp index 882c1268..81e938f7 100644 --- a/src/SchemaNode.cpp +++ b/src/SchemaNode.cpp @@ -6,6 +6,7 @@ * SPDX-License-Identifier: BSD-3-Clause */ +#include #include #include #include @@ -361,6 +362,19 @@ std::vector SchemaNode::when() const return res; } +/** + * @brief Retrieves the list of extension instances. + */ +std::vector SchemaNode::extensionInstances() const +{ + std::vector res; + auto span = std::span(m_node->exts, LY_ARRAY_COUNT(m_node->exts)); + std::transform(span.begin(), span.end(), std::back_inserter(res), [this](const lysc_ext_instance& ext) { + return ExtensionInstance(&ext, m_ctx); + }); + return res; +} + /** * @brief Print the (sub)schema of this schema node * diff --git a/tests/example_schema.hpp b/tests/example_schema.hpp index 2408f76c..89c76040 100644 --- a/tests/example_schema.hpp +++ b/tests/example_schema.hpp @@ -733,3 +733,24 @@ module with-inet-types { } } )"s; + +const auto with_extensions_module = R"( +module with-extensions { + yang-version 1.1; + prefix "we"; + namespace "we"; + import ietf-netconf-acm { + prefix "nacm"; + } + extension annotation { + argument name; + description "This is inspired by md:annotation"; + } + container c { + nacm:default-deny-write; + we:annotation last-modified { + type yang:date-and-time; + } + } +} +)"s; diff --git a/tests/schema_node.cpp b/tests/schema_node.cpp index e40edb3c..12d64709 100644 --- a/tests/schema_node.cpp +++ b/tests/schema_node.cpp @@ -349,6 +349,17 @@ TEST_CASE("SchemaNode") REQUIRE(actualPaths == expectedPaths); } + DOCTEST_SUBCASE("SchemaNode::extensionInstances") + { + ctx->setSearchDir(TESTS_DIR / "yang"); + auto mod = ctx->parseModule(with_extensions_module, libyang::SchemaFormat::YANG); + REQUIRE(mod.extensionInstances().size() == 0); + auto elem = ctx->findPath("/with-extensions:c"); + REQUIRE(elem.extensionInstances().size() == 2); + REQUIRE(elem.extensionInstances()[0].definition().name() == "default-deny-write"); + REQUIRE(elem.extensionInstances()[1].definition().name() == "annotation"); + } + DOCTEST_SUBCASE("SchemaNode::operator==") { auto a = ctx->findPath("/type_module:leafString"); From 9ca3b79c4b3a26ed45f5adaf92d8066a0006da66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bed=C5=99ich=20Schindler?= Date: Mon, 23 Sep 2024 18:43:04 +0200 Subject: [PATCH 26/57] don't crash on extensions with no argument name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jan Kundrát Change-Id: I531fcd87da518985bf8f730675b3d20c197224db --- include/libyang-cpp/Module.hpp | 2 +- src/Module.cpp | 5 ++++- tests/schema_node.cpp | 2 ++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/include/libyang-cpp/Module.hpp b/include/libyang-cpp/Module.hpp index a3fb7e13..acee80e5 100644 --- a/include/libyang-cpp/Module.hpp +++ b/include/libyang-cpp/Module.hpp @@ -161,7 +161,7 @@ class LIBYANG_CPP_EXPORT Identity { class LIBYANG_CPP_EXPORT ExtensionInstance { public: Extension definition() const; - std::string argument() const; + std::optional argument() const; private: ExtensionInstance(const lysc_ext_instance* instance, std::shared_ptr ctx); diff --git a/src/Module.cpp b/src/Module.cpp index 79faa5c8..416ec8df 100644 --- a/src/Module.cpp +++ b/src/Module.cpp @@ -399,8 +399,11 @@ ExtensionInstance::ExtensionInstance(const lysc_ext_instance* instance, std::sha * * Wraps `lysc_ext_instance::argument`. */ -std::string ExtensionInstance::argument() const +std::optional ExtensionInstance::argument() const { + if (!m_instance->argument) { + return std::nullopt; + } return m_instance->argument; } diff --git a/tests/schema_node.cpp b/tests/schema_node.cpp index 12d64709..c5dffd87 100644 --- a/tests/schema_node.cpp +++ b/tests/schema_node.cpp @@ -357,7 +357,9 @@ TEST_CASE("SchemaNode") auto elem = ctx->findPath("/with-extensions:c"); REQUIRE(elem.extensionInstances().size() == 2); REQUIRE(elem.extensionInstances()[0].definition().name() == "default-deny-write"); + REQUIRE(!elem.extensionInstances()[0].argument()); REQUIRE(elem.extensionInstances()[1].definition().name() == "annotation"); + REQUIRE(elem.extensionInstances()[1].argument() == "last-modified"); } DOCTEST_SUBCASE("SchemaNode::operator==") From ca36a0a6250765c1b374653a20623ab3ec8e5680 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= Date: Mon, 23 Sep 2024 19:58:01 +0200 Subject: [PATCH 27/57] allow retrieval of YANG module which holds the extension definition MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This was inspired by a patch from Bedrich (Ib0dfb68afaf0f74d3defe87b16b7c34457cc4899). I wanted a little more incremental approach. Co-authored-by: Bedřich Schindler Change-Id: I58ef601763e0b55b609a12357868817a042fb83d --- include/libyang-cpp/Module.hpp | 2 ++ src/Module.cpp | 13 +++++++++++++ tests/schema_node.cpp | 2 ++ 3 files changed, 17 insertions(+) diff --git a/include/libyang-cpp/Module.hpp b/include/libyang-cpp/Module.hpp index acee80e5..e33b8275 100644 --- a/include/libyang-cpp/Module.hpp +++ b/include/libyang-cpp/Module.hpp @@ -95,6 +95,7 @@ class LIBYANG_CPP_EXPORT Module { friend Context; friend DataNode; + friend Extension; friend Meta; friend Identity; friend SchemaNode; @@ -182,6 +183,7 @@ class LIBYANG_CPP_EXPORT ExtensionInstance { */ class LIBYANG_CPP_EXPORT Extension { public: + Module module() const; std::string name() const; private: diff --git a/src/Module.cpp b/src/Module.cpp index 416ec8df..383bf947 100644 --- a/src/Module.cpp +++ b/src/Module.cpp @@ -423,6 +423,19 @@ Extension::Extension(const lysc_ext* ext, std::shared_ptr ctx) { } +/** + * @brief Returns the module in which this extension was defined + * + * An extension that's defined in module A might be instantiated in many places + * in many modules, and possibly also under many schema nodes. + * + * Wraps `lysc_ext::module`. + */ +Module Extension::module() const +{ + return Module{m_ext->module, m_ctx}; +} + /** * @brief Returns the name of the extension definition * diff --git a/tests/schema_node.cpp b/tests/schema_node.cpp index c5dffd87..b4a353fa 100644 --- a/tests/schema_node.cpp +++ b/tests/schema_node.cpp @@ -356,8 +356,10 @@ TEST_CASE("SchemaNode") REQUIRE(mod.extensionInstances().size() == 0); auto elem = ctx->findPath("/with-extensions:c"); REQUIRE(elem.extensionInstances().size() == 2); + REQUIRE(elem.extensionInstances()[0].definition().module().name() == "ietf-netconf-acm"); REQUIRE(elem.extensionInstances()[0].definition().name() == "default-deny-write"); REQUIRE(!elem.extensionInstances()[0].argument()); + REQUIRE(elem.extensionInstances()[1].definition().module().name() == "with-extensions"); REQUIRE(elem.extensionInstances()[1].definition().name() == "annotation"); REQUIRE(elem.extensionInstances()[1].argument() == "last-modified"); } From c92f06dc392a47521167bab403330a8cd7316d1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= Date: Mon, 23 Sep 2024 19:58:52 +0200 Subject: [PATCH 28/57] multiple modules might provide an extension with the same name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ...especially in case of augments. In this test case, the other module "adds" an extension instance with the same unqualified name as an existing extension instance. This was inspired by a patch from Bedrich (Ib0dfb68afaf0f74d3defe87b16b7c34457cc4899). I was wondering why that one was adding the .module() method for both Extension and ExtensionInstance; now I know. Co-authored-by: Bedřich Schindler Change-Id: Ib839695a67b3b102f487b18a835c9c903e0ffdcd --- include/libyang-cpp/Module.hpp | 2 ++ src/Module.cpp | 10 ++++++++++ tests/example_schema.hpp | 13 +++++++++++++ tests/schema_node.cpp | 20 ++++++++++++++++++++ 4 files changed, 45 insertions(+) diff --git a/include/libyang-cpp/Module.hpp b/include/libyang-cpp/Module.hpp index e33b8275..06c6262f 100644 --- a/include/libyang-cpp/Module.hpp +++ b/include/libyang-cpp/Module.hpp @@ -96,6 +96,7 @@ class LIBYANG_CPP_EXPORT Module { friend Context; friend DataNode; friend Extension; + friend ExtensionInstance; friend Meta; friend Identity; friend SchemaNode; @@ -161,6 +162,7 @@ class LIBYANG_CPP_EXPORT Identity { */ class LIBYANG_CPP_EXPORT ExtensionInstance { public: + Module module() const; Extension definition() const; std::optional argument() const; diff --git a/src/Module.cpp b/src/Module.cpp index 383bf947..cfd75c91 100644 --- a/src/Module.cpp +++ b/src/Module.cpp @@ -390,6 +390,16 @@ ExtensionInstance::ExtensionInstance(const lysc_ext_instance* instance, std::sha { } +/** + * @brief Returns the module of this extension instance. + * + * Wraps `lysc_ext_instance::module`. + */ +Module ExtensionInstance::module() const +{ + return Module{m_instance->module, m_ctx}; +} + /** * @brief Returns the argument name * diff --git a/tests/example_schema.hpp b/tests/example_schema.hpp index 89c76040..41d92b64 100644 --- a/tests/example_schema.hpp +++ b/tests/example_schema.hpp @@ -754,3 +754,16 @@ module with-extensions { } } )"s; +const auto augmented_extensions_module = R"( +module augmenting-extensions { + yang-version 1.1; + prefix "ae"; + namespace "ae"; + import with-extensions { + prefix "we"; + } + augment "/we:c" { + we:annotation last-modified; + } +} +)"s; diff --git a/tests/schema_node.cpp b/tests/schema_node.cpp index b4a353fa..6e20a424 100644 --- a/tests/schema_node.cpp +++ b/tests/schema_node.cpp @@ -356,12 +356,32 @@ TEST_CASE("SchemaNode") REQUIRE(mod.extensionInstances().size() == 0); auto elem = ctx->findPath("/with-extensions:c"); REQUIRE(elem.extensionInstances().size() == 2); + REQUIRE(elem.extensionInstances()[0].module().name() == "with-extensions"); REQUIRE(elem.extensionInstances()[0].definition().module().name() == "ietf-netconf-acm"); REQUIRE(elem.extensionInstances()[0].definition().name() == "default-deny-write"); REQUIRE(!elem.extensionInstances()[0].argument()); + REQUIRE(elem.extensionInstances()[1].module().name() == "with-extensions"); REQUIRE(elem.extensionInstances()[1].definition().module().name() == "with-extensions"); REQUIRE(elem.extensionInstances()[1].definition().name() == "annotation"); REQUIRE(elem.extensionInstances()[1].argument() == "last-modified"); + + auto mod2 = ctx->parseModule(augmented_extensions_module, libyang::SchemaFormat::YANG); + REQUIRE(mod2.extensionInstances().size() == 0); + elem = ctx->findPath("/with-extensions:c"); + REQUIRE(elem.extensionInstances().size() == 3); + // the augment adds a new extension, and libyang places that at index 0 + REQUIRE(elem.extensionInstances()[0].module().name() == "augmenting-extensions"); + REQUIRE(elem.extensionInstances()[0].definition().module().name() == "with-extensions"); + REQUIRE(elem.extensionInstances()[0].definition().name() == "annotation"); + REQUIRE(elem.extensionInstances()[0].argument() == "last-modified"); + REQUIRE(elem.extensionInstances()[1].module().name() == "with-extensions"); + REQUIRE(elem.extensionInstances()[1].definition().module().name() == "ietf-netconf-acm"); + REQUIRE(elem.extensionInstances()[1].definition().name() == "default-deny-write"); + REQUIRE(!elem.extensionInstances()[1].argument()); + REQUIRE(elem.extensionInstances()[2].module().name() == "with-extensions"); + REQUIRE(elem.extensionInstances()[2].definition().module().name() == "with-extensions"); + REQUIRE(elem.extensionInstances()[2].definition().name() == "annotation"); + REQUIRE(elem.extensionInstances()[2].argument() == "last-modified"); } DOCTEST_SUBCASE("SchemaNode::operator==") From 0ef451624fe4b3c307954ebcbe1b34cbaf2f56f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= Date: Mon, 23 Sep 2024 20:36:00 +0200 Subject: [PATCH 29/57] extension instances and extension definitions can be extended MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This was inspired by a patch from Bedrich (Ib0dfb68afaf0f74d3defe87b16b7c34457cc4899); I preserved the implementation, but I really wanted to document what is going on. Change-Id: I228430278de1cfa7ac192574d25e50cf157c70a1 Co-authored-by: Bedřich Schindler --- include/libyang-cpp/Module.hpp | 3 +++ src/Module.cpp | 28 ++++++++++++++++++++++++++++ tests/example_schema.hpp | 8 +++++++- tests/schema_node.cpp | 13 +++++++++++++ 4 files changed, 51 insertions(+), 1 deletion(-) diff --git a/include/libyang-cpp/Module.hpp b/include/libyang-cpp/Module.hpp index 06c6262f..f10c36fc 100644 --- a/include/libyang-cpp/Module.hpp +++ b/include/libyang-cpp/Module.hpp @@ -165,6 +165,7 @@ class LIBYANG_CPP_EXPORT ExtensionInstance { Module module() const; Extension definition() const; std::optional argument() const; + std::vector extensionInstances() const; private: ExtensionInstance(const lysc_ext_instance* instance, std::shared_ptr ctx); @@ -175,6 +176,7 @@ class LIBYANG_CPP_EXPORT ExtensionInstance { friend Module; friend Context; friend DataNode; + friend Extension; friend SchemaNode; }; @@ -187,6 +189,7 @@ class LIBYANG_CPP_EXPORT Extension { public: Module module() const; std::string name() const; + std::vector extensionInstances() const; private: Extension(const lysc_ext* def, std::shared_ptr ctx); diff --git a/src/Module.cpp b/src/Module.cpp index cfd75c91..4dc9e3bd 100644 --- a/src/Module.cpp +++ b/src/Module.cpp @@ -427,6 +427,20 @@ Extension ExtensionInstance::definition() const return Extension{m_instance->def, m_ctx}; } +/** + * @brief Returns instances of extensions which are extending this particular extensions instance + * + * Wraps `lysc_ext_instance::exts`. + */ +std::vector ExtensionInstance::extensionInstances() const +{ + std::vector res; + for (const auto& ext : std::span(m_instance->exts, LY_ARRAY_COUNT(m_instance->exts))) { + res.emplace_back(ExtensionInstance{&ext, m_ctx}); + } + return res; +} + Extension::Extension(const lysc_ext* ext, std::shared_ptr ctx) : m_ext(ext) , m_ctx(ctx) @@ -455,4 +469,18 @@ std::string Extension::name() const { return m_ext->name; } + +/** + * @brief Returns all extension instances which extend this extension definition + * + * Wraps `lysc_ext::exts`. + */ +std::vector Extension::extensionInstances() const +{ + std::vector res; + for (const auto& ext : std::span(m_ext->exts, LY_ARRAY_COUNT(m_ext->exts))) { + res.emplace_back(ExtensionInstance{&ext, m_ctx}); + } + return res; +} } diff --git a/tests/example_schema.hpp b/tests/example_schema.hpp index 41d92b64..c093f50f 100644 --- a/tests/example_schema.hpp +++ b/tests/example_schema.hpp @@ -762,8 +762,14 @@ module augmenting-extensions { import with-extensions { prefix "we"; } + extension another-annotation { + we:annotation wtf-is-this; + } augment "/we:c" { - we:annotation last-modified; + we:annotation last-modified { + ae:another-annotation { + } + } } } )"s; diff --git a/tests/schema_node.cpp b/tests/schema_node.cpp index 6e20a424..a86900da 100644 --- a/tests/schema_node.cpp +++ b/tests/schema_node.cpp @@ -374,6 +374,19 @@ TEST_CASE("SchemaNode") REQUIRE(elem.extensionInstances()[0].definition().module().name() == "with-extensions"); REQUIRE(elem.extensionInstances()[0].definition().name() == "annotation"); REQUIRE(elem.extensionInstances()[0].argument() == "last-modified"); + // a funny thing about an extension instance is that it can be also extended... + REQUIRE(elem.extensionInstances()[0].extensionInstances().size() == 1); + REQUIRE(elem.extensionInstances()[0].extensionInstances()[0].module().name() == "augmenting-extensions"); + REQUIRE(elem.extensionInstances()[0].extensionInstances()[0].definition().module().name() == "augmenting-extensions"); + REQUIRE(elem.extensionInstances()[0].extensionInstances()[0].definition().name() == "another-annotation"); + REQUIRE(!elem.extensionInstances()[0].extensionInstances()[0].argument()); + // ...and of course extension definitions are no exception and can be extended as well, yay! + REQUIRE(elem.extensionInstances()[0].extensionInstances()[0].definition().extensionInstances().size() == 1); + REQUIRE(elem.extensionInstances()[0].extensionInstances()[0].definition().extensionInstances()[0].module().name() == "augmenting-extensions"); + REQUIRE(elem.extensionInstances()[0].extensionInstances()[0].definition().extensionInstances()[0].definition().module().name() == "with-extensions"); + REQUIRE(elem.extensionInstances()[0].extensionInstances()[0].definition().extensionInstances()[0].definition().name() == "annotation"); + REQUIRE(elem.extensionInstances()[0].extensionInstances()[0].definition().extensionInstances()[0].argument() == "wtf-is-this"); + // OK, enough with that. These are the "old" extensions from the original module pre-augmentation: REQUIRE(elem.extensionInstances()[1].module().name() == "with-extensions"); REQUIRE(elem.extensionInstances()[1].definition().module().name() == "ietf-netconf-acm"); REQUIRE(elem.extensionInstances()[1].definition().name() == "default-deny-write"); From f3cd4e05462a16e81d6bfd0c4a5b385cf88a8549 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= Date: Tue, 17 Sep 2024 11:10:04 +0200 Subject: [PATCH 30/57] start setting the SOVERSION Some distros out there scream out loudly when there are public SOs with no SOVERSION. Since we do not want to promise any long-term API/ABI stability, we can start bumping version/soversion rather aggressively, with no pretense of semver. Bug: https://github.com/CESNET/libyang-cpp/issues/3 Change-Id: I10dafa54db33dd7b020bd9473977ca0ef0fad46a --- CMakeLists.txt | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 94ac7168..3d868099 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) -prepare_git_version(LIBYANG_CPP_VERSION "0.0") +prepare_git_version(LIBYANG_CPP_VERSION "3") find_package(Doxygen) option(WITH_DOCS "Create and install internal documentation (needs Doxygen)" ${DOXYGEN_FOUND}) @@ -29,7 +29,7 @@ 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.4.2 IMPORTED_TARGET) -set(LIBYANG_CPP_PKG_VERSION "2") +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) @@ -64,6 +64,12 @@ add_library(yang-cpp ) target_link_libraries(yang-cpp PRIVATE PkgConfig::LIBYANG) +# 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. +set_target_properties(yang-cpp PROPERTIES + VERSION ${LIBYANG_CPP_PKG_VERSION} + SOVERSION ${LIBYANG_CPP_PKG_VERSION}) include(GenerateExportHeader) generate_export_header(yang-cpp BASE_NAME libyang_cpp EXPORT_FILE_NAME libyang-cpp/export.h) From d0f6422fee7a46fcb7445c88f499f61b3eb0ead0 Mon Sep 17 00:00:00 2001 From: Adam Piecek Date: Wed, 23 Oct 2024 14:37:09 +0200 Subject: [PATCH 31/57] added support for RpcYang in Context::parseOp Change-Id: I25182ea2d042be1e6e4246e18aee260cc032e547 --- 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 b844bd91..287f8c82 100644 --- a/src/Context.cpp +++ b/src/Context.cpp @@ -221,7 +221,8 @@ std::optional 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 71ae8738..11019ebb 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"); From 7e015f3486bdbb54f1dcc2e2ce51102b1d623081 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= Date: Wed, 23 Oct 2024 12:52:24 +0200 Subject: [PATCH 32/57] throw when lyd_validate_all returns error Bug: https://github.com/CESNET/libyang-cpp/issues/20 Change-Id: I005a2f1b057978573a4046e7b4cc31d77e36fde3 --- 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 51c86c8b..2ef17f25 100644 --- a/src/DataNode.cpp +++ b/src/DataNode.cpp @@ -1170,7 +1170,9 @@ void validateAll(std::optional& node, const std::optionalm_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 a23e4c2a..8a2610ec 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); From 490d8bb242d33213b948485f5b94c55e22cf86a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= Date: Thu, 21 Nov 2024 11:32:44 +0100 Subject: [PATCH 33/57] remove a misleading comment 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 --- src/SchemaNode.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/SchemaNode.cpp b/src/SchemaNode.cpp index 81e938f7..9934cea4 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(&reinterpret_cast(m_node)->input), m_ctx}; } From e1b17386cf61048d2fe27fffb3b763981a225f52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bed=C5=99ich=20Schindler?= Date: Wed, 27 Nov 2024 09:47:47 +0100 Subject: [PATCH 34/57] schema: improve `List::keys()` not to use `std::move` `List::keys()` used `std::move` while iterating over array of leafs. This was solved without using `std::move`. Change-Id: I8cbf8780ecd8848e46c1de5d4123a08624536bba --- src/SchemaNode.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/SchemaNode.cpp b/src/SchemaNode.cpp index 9934cea4..20e2affc 100644 --- a/src/SchemaNode.cpp +++ b/src/SchemaNode.cpp @@ -593,8 +593,7 @@ std::vector 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)); } } From 1102ecdcafbc9206f59b383769687e418557838e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bed=C5=99ich=20Schindler?= Date: Mon, 25 Nov 2024 15:54:02 +0100 Subject: [PATCH 35/57] schema: make leaf-list's `default` statement available 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 --- 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 83afc067..8ddf9be6 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 defaultValuesStr() const; libyang::types::constraints::ListSize maxElements() const; libyang::types::constraints::ListSize minElements() const; std::optional units() const; diff --git a/src/SchemaNode.cpp b/src/SchemaNode.cpp index 9934cea4..95bc09b5 100644 --- a/src/SchemaNode.cpp +++ b/src/SchemaNode.cpp @@ -472,6 +472,23 @@ types::Type LeafList::valueType() const return types::Type{reinterpret_cast(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 LeafList::defaultValuesStr() const +{ + auto dflts = reinterpret_cast(m_node)->dflts; + std::vector 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 11019ebb..902faf62 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 c093f50f..2861b1af 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 a86900da..80c74070 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{"-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); From 01f2633cef60495d5cafc4b4b1f25273b03ab3cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bed=C5=99ich=20Schindler?= Date: Tue, 22 Oct 2024 15:11:30 +0200 Subject: [PATCH 36/57] schema: Make choice and case statements available MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- 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 8ddf9be6..0f1a4c42 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 @@ class LIBYANG_CPP_EXPORT SchemaNode { // 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 @@ class LIBYANG_CPP_EXPORT AnyDataAnyXML : public SchemaNode { 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 cases() const; + std::optional 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 bd204029..26b5099f 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 Choice::cases() const +{ + auto choice = reinterpret_cast(m_node); + auto cases = reinterpret_cast(choice->cases); + std::vector 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 Choice::defaultCase() const +{ + auto choice = reinterpret_cast(m_node); + if (!choice->dflt) { + return std::nullopt; + } + return Case{reinterpret_cast(choice->dflt), m_ctx}; +} + /** * @brief Checks whether this container is mandatory. * diff --git a/tests/context.cpp b/tests/context.cpp index 902faf62..5929b751 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 2861b1af..ae3b4def 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 80c74070..8d74bd2c 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 defaultCase; + std::vector caseNames; + std::optional 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 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()); From a1acdc794facf8cbf113f73274ecebd5898c81a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= Date: Tue, 17 Dec 2024 15:08:43 +0100 Subject: [PATCH 37/57] Wrap lyd_change_term for changing the value for a terminal node Previously, the code would require a newPath(..., libyang::CreationOptions::Update), which is quite a mouthful. Change-Id: I8a908c0fdd3e48dda830819758522a511adedd3b --- 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 22114155..851681b4 100644 --- a/include/libyang-cpp/DataNode.hpp +++ b/include/libyang-cpp/DataNode.hpp @@ -212,6 +212,14 @@ class LIBYANG_CPP_EXPORT DataNodeTerm : public DataNode { 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 2ef17f25..84591e5f 100644 --- a/src/DataNode.cpp +++ b/src/DataNode.cpp @@ -903,6 +903,27 @@ types::Type DataNodeTerm::valueType() const return impl(reinterpret_cast(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 8a2610ec..45fd6c1a 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") From 32b200ed06e9adb44a8d4ce6771f18812a54d06e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bed=C5=99ich=20Schindler?= Date: Wed, 20 Nov 2024 10:20:19 +0100 Subject: [PATCH 38/57] Add `Module::child()`, `Module::childrenDfs()` and `Module::immediateChildren()` 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 --- 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 557a0a2c..4324791c 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; + friend Module; friend SchemaNode; ~Collection(); Collection(const Collection&); diff --git a/include/libyang-cpp/Module.hpp b/include/libyang-cpp/Module.hpp index f10c36fc..ab20d364 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 +class Collection; namespace types { class IdentityRef; @@ -86,7 +88,10 @@ class LIBYANG_CPP_EXPORT Module { std::vector identities() const; + std::optional child() const; ChildInstanstiables childInstantiables() const; + libyang::Collection childrenDfs() const; + Collection immediateChildren() const; std::vector actionRpcs() const; std::string printStr(const SchemaOutputFormat format, const std::optional flags = std::nullopt, std::optional lineLength = std::nullopt) const; diff --git a/src/Module.cpp b/src/Module.cpp index 4dc9e3bd..d6d40238 100644 --- a/src/Module.cpp +++ b/src/Module.cpp @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -178,6 +179,23 @@ std::vector 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 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 Module::childrenDfs() const +{ + if (!m_module->implemented) { + throw Error{"Module::childrenDfs: module is not implemented"}; + } + return Collection{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 Module::immediateChildren() const +{ + auto c = child(); + return c ? c->siblings() : Collection{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 5929b751..25343db0 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 ae3b4def..0d8acb99 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 8d74bd2c..00013771 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 expectedPaths; - std::optional 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 expectedPaths; + std::optional 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 actualPaths; + for (const auto& child : *children) { + actualPaths.emplace_back(child.path()); + } - std::vector 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 expectedPaths; + DOCTEST_SUBCASE("implemented module") + { + std::vector expectedPaths; + std::optional> 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 actualPaths; + for (const auto& it : *children) { + actualPaths.emplace_back(it.path()); + } - path = "/type_module:leafString"; + REQUIRE(actualPaths == expectedPaths); } - std::vector 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 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 expectedPaths; + std::optional> 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 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 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 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") From 39c7530caa510144c17521278b721ba1e6d8ff40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= Date: Thu, 9 Jan 2025 15:31:37 +0100 Subject: [PATCH 39/57] upstream stopped reporting schema-mounts node Change-Id: I940769d38d56fcfda3e1408c92331fdb00c161e9 --- CMakeLists.txt | 2 +- tests/context.cpp | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 3d868099..732f52b5 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 5929b751..9d38feae 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": {} + } } )"); } From 82c963766ad4e4a802db7be656acbedb640745e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= Date: Thu, 30 Jan 2025 16:34:12 +0100 Subject: [PATCH 40/57] CI: temporarily pin version due to anyxml behavior changes upstream Change-Id: Id977f4d045098c1b93656c0efb871c9a1b650e2d --- .zuul.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.zuul.yaml b/.zuul.yaml index b41c4904..19f0deff 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: From 50a216e35a555961f94a32a71bb2d45ac611d0aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= Date: Wed, 29 Jan 2025 22:49:08 +0100 Subject: [PATCH 41/57] build: a single place to define package version Change-Id: I2cd7397895ed4852f852e99b97543dde76eaff8f --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 732f52b5..c518ca89 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) From 1533458346b4f395b1187c646b61bbcb1fddc615 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= Date: Mon, 4 Nov 2024 14:52:12 +0100 Subject: [PATCH 42/57] YANG-flavored regular expressions Change-Id: I93b2756d0f470585280c076308df3f384bd7765d --- 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 c518ca89..512af8cf 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 00000000..31935f2d --- /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 + * + * SPDX-License-Identifier: BSD-3-Clause + */ +#pragma once +#include +#include + +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 00000000..a34fcd5c --- /dev/null +++ b/src/Regex.cpp @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2025 CESNET, https://photonics.cesnet.cz/ + * + * Written by Jan Kundrát + * + * 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 + +#include +#include +#include "utils/exception.hpp" + +#define THE_PCRE2_CODE_P reinterpret_cast(this->code) +#define THE_PCRE2_CODE_P_P reinterpret_cast(&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 00000000..7594f431 --- /dev/null +++ b/tests/regex.cpp @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2025 CESNET, https://photonics.cesnet.cz/ + * + * Written by Jan Kundrát + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +#include +#include +#include + +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 :(. +} From 8d406728a53c2e77a4fe7393b7e30d42b8f9b9bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= Date: Thu, 30 Jan 2025 15:40:22 +0100 Subject: [PATCH 43/57] Adapt to upstream changes in anyxml JSON printing Change-Id: I5f6de28cebc95a446549017c2768b450f4fd6526 --- .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 19f0deff..b41c4904 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 512af8cf..a40fd529 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 45fd6c1a..14470dd9 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"|()|"s); + == R"|()|"s + origJSON + ""); } REQUIRE(!!val); From f050e7e4a17ef2e221ca000a544042c33c9541fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= Date: Thu, 13 Mar 2025 19:21:26 +0100 Subject: [PATCH 44/57] fix DataNode::insertSibling() return value 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 Bug: https://github.com/CESNET/libyang-cpp/issues/29 Change-Id: Id7f84a31e50212d6e2cbce9ab03a351a3721f767 --- 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 84591e5f..b899b183 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 14470dd9..b6ee4558 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); From f958af42bf5d9fbd901ed59ebc1359ac0ddcc00f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= Date: Fri, 14 Mar 2025 11:36:50 +0100 Subject: [PATCH 45/57] API/ABI change: opaque node naming 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 --- 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 a40fd529..c5fec452 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 baa47b30..ca890632 100644 --- a/include/libyang-cpp/Context.hpp +++ b/include/libyang-cpp/Context.hpp @@ -115,8 +115,8 @@ class LIBYANG_CPP_EXPORT Context { CreatedNodes newPath2(const std::string& path, libyang::JSON json, const std::optional options = std::nullopt) const; CreatedNodes newPath2(const std::string& path, libyang::XML xml, const std::optional options = std::nullopt) const; std::optional newExtPath(const ExtensionInstance& ext, const std::string& path, const std::optional& value, const std::optional options = std::nullopt) const; - std::optional newOpaqueJSON(const std::string& moduleName, const std::string& name, const std::optional& value) const; - std::optional newOpaqueXML(const std::string& moduleName, const std::string& name, const std::optional& value) const; + std::optional newOpaqueJSON(const OpaqueName& name, const std::optional& value) const; + std::optional newOpaqueXML(const OpaqueName& name, const std::optional& value) const; SchemaNode findPath(const std::string& dataPath, const InputOutputNodes inputOutputNodes = InputOutputNodes::Input) const; Set findXPath(const std::string& path) const; diff --git a/include/libyang-cpp/DataNode.hpp b/include/libyang-cpp/DataNode.hpp index 851681b4..310b5e38 100644 --- a/include/libyang-cpp/DataNode.hpp +++ b/include/libyang-cpp/DataNode.hpp @@ -225,15 +225,22 @@ class LIBYANG_CPP_EXPORT DataNodeTerm : public DataNode { }; /** - * @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 prefix; std::string name; + std::string pretty() const; }; /** diff --git a/src/Context.cpp b/src/Context.cpp index 287f8c82..fec2f271 100644 --- a/src/Context.cpp +++ b/src/Context.cpp @@ -378,17 +378,25 @@ std::optional 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 Context::newOpaqueJSON(const std::string& moduleName, const std::string& name, const std::optional& value) const +std::optional Context::newOpaqueJSON(const OpaqueName& name, const std::optional& 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(m_ctx)}; @@ -403,17 +411,23 @@ std::optional 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 Context::newOpaqueXML(const std::string& xmlNamespace, const std::string& name, const std::optional& value) const +std::optional Context::newOpaqueXML(const OpaqueName& name, const std::optional& 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(m_ctx)}; diff --git a/src/DataNode.cpp b/src/DataNode.cpp index b899b183..344f1b68 100644 --- a/src/DataNode.cpp +++ b/src/DataNode.cpp @@ -1112,9 +1112,9 @@ OpaqueName DataNodeOpaque::name() const { auto opaq = reinterpret_cast(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(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 b6ee4558..a1096a66 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); From 8c59ecc5c687f8ee2ce62825835a378b422185f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= Date: Fri, 14 Mar 2025 13:41:01 +0100 Subject: [PATCH 46/57] Add a helper for finding opaque nodes 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 --- 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 310b5e38..50d6c0e7 100644 --- a/include/libyang-cpp/DataNode.hpp +++ b/include/libyang-cpp/DataNode.hpp @@ -102,6 +102,7 @@ class LIBYANG_CPP_EXPORT DataNode { bool isOpaque() const; DataNodeOpaque asOpaque() const; + std::optional firstOpaqueSibling() const; // TODO: allow setting the `parent` argument DataNode duplicate(const std::optional opts = std::nullopt) const; @@ -241,6 +242,7 @@ struct LIBYANG_CPP_EXPORT OpaqueName { std::optional prefix; std::string name; std::string pretty() const; + bool matches(const std::string& prefixIsh, const std::string& name) const; }; /** diff --git a/src/DataNode.cpp b/src/DataNode.cpp index 344f1b68..7e87917a 100644 --- a/src/DataNode.cpp +++ b/src/DataNode.cpp @@ -1135,6 +1135,20 @@ std::string OpaqueName::pretty() const } } +/** + * @short Fuzzy-match a real-world name against a combination of "something like a prefix" and "unqualified name" + * + * Because libyang doesn't propagate inherited prefixes, and because opaque nodes are magic, we seem to require + * this "fuzzy matching". It won't properly report a match on opaque nodes with a prefix that's inherited when + * using XML namespaces, though. + * */ +bool OpaqueName::matches(const std::string& prefixIsh, const std::string& name) const +{ + return name == this->name + && (prefixIsh == moduleOrNamespace + || (!!prefix && prefixIsh == *prefix)); +} + /** * Wraps a raw non-null lyd_node pointer. * @param node The pointer to be wrapped. Must not be null. @@ -1305,4 +1319,39 @@ bool DataNode::siblingsEqual(const libyang::DataNode& other, const DataCompare f } } +/** + * @short Find the first opaque node among the siblings + * + * This function was inspired by `lyd_find_sibling_opaq_next()`. + * */ +std::optional DataNode::firstOpaqueSibling() const +{ + struct lyd_node *candidate = m_node; + + // Skip all non-opaque nodes; libyang guarantees to have them first, followed by a (possibly empty) set + // of opaque nodes. This is not documented anywhere, but it was explicitly confirmed by the maintainer: + // + // JK: can I rely on all non-opaque nodes being listed first among the siblings, and then all opaque nodes + // in one continuous sequence (but with an unspecified order among the opaque nodes themselves)? + // + // MV: yep + while (candidate && candidate->schema) { + candidate = candidate->next; + } + + // walk back through the opaque nodes; however, libyang lists are not your regular linked lists + while (candidate + && !candidate->prev->schema // don't go from the first opaque node through the non-opaque ones + && candidate->prev->next // don't wrap from the first node to the last one in case all of them are opaque + ) { + candidate = candidate->prev; + } + + if (candidate) { + return DataNode{candidate, m_refs}.asOpaque(); + } + + return std::nullopt; +} + } diff --git a/tests/data_node.cpp b/tests/data_node.cpp index a1096a66..db5a28eb 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 00000000..f5bc32c6 --- /dev/null +++ b/tests/yang/sysrepo@2024-10-25.yang @@ -0,0 +1,257 @@ +module sysrepo { + namespace "http://www.sysrepo.org/yang/sysrepo"; + prefix sr; + + yang-version 1.1; + + import ietf-yang-types { + prefix yang; + } + + import ietf-datastores { + prefix ds; + } + + import ietf-yang-metadata { + prefix md; + revision-date 2016-08-05; + } + + organization + "CESNET"; + + contact + "Author: Michal Vasko + "; + + description + "Sysrepo YANG datastore internal attributes and information."; + + revision "2024-10-25" { + description + "Removed redundant metadata used for push operational data."; + } + + revision "2019-07-10" { + description + "Initial revision."; + } + + typedef module-ref { + description + "Reference to a module."; + type leafref { + path "/sysrepo-modules/module/name"; + } + } + + md:annotation operation { + type enumeration { + enum none { + description + "Node with this operation must exist but does not affect the datastore in any way."; + reference + "RFC 6241 section 7.2.: default-operation"; + } + enum ether { + description + "Node with this operation does not have to exist and does not affect the datastore in any way."; + } + enum purge { + description + "Node with this operation represents an arbitrary generic node instance and all + the instances will be deleted."; + } + } + description + "Additional proprietary operations used internally."; + reference + "RFC 6241 section 7.2."; + } + + identity notification { + base ds:datastore; + description + "Special datastore for storing notifications for replay."; + } + + grouping module-info-grp { + leaf name { + type string; + description + "Module name."; + } + + leaf revision { + type string; + description + "Module revision."; + } + + leaf-list enabled-feature { + type string; + description + "List of all the enabled features."; + } + + list plugin { + key "datastore"; + description + "Module datastore plugin handling specific datastore data."; + + leaf datastore { + type identityref { + base ds:datastore; + } + description + "Datastore of this plugin."; + } + + leaf name { + type string; + mandatory true; + description + "Specific plugin name as present in the plugin structures."; + } + } + } + + grouping deps-grp { + list lref { + description + "Dependency of a leafref node."; + + leaf target-path { + type yang:xpath1.0; + mandatory true; + description + "Path identifying the leafref target node."; + } + + leaf target-module { + type module-ref; + mandatory true; + description + "Foreign target module of the leafref."; + } + } + + list inst-id { + description + "Dependency of an instance-identifier node."; + + leaf source-path { + type yang:xpath1.0; + mandatory true; + description + "Path identifying the instance-identifier node."; + } + + leaf default-target-path { + type yang:xpath1.0; + description + "Default instance-identifier value."; + } + } + + list xpath { + description + "Dependency of an XPath expression - must or when statement."; + + leaf expression { + type yang:xpath1.0; + mandatory true; + description + "XPath expression of the dependency - must or when statement argument."; + } + + leaf-list target-module { + type module-ref; + description + "Foreign modules with the data needed for evaluation of the XPath."; + } + } + } + + container sysrepo-modules { + config false; + description + "All installed Sysrepo modules."; + + leaf content-id { + type uint32; + mandatory true; + description + "Sysrepo module-set content-id to be used for its generated yang-library data."; + } + + list module { + key "name"; + description + "Sysrepo module."; + + uses module-info-grp; + + leaf replay-support { + type yang:date-and-time; + description + "Present only if the module supports replay. Means the earliest stored notification if any present. + Otherwise the time the replay support was switched on."; + } + + container deps { + description + "Module data dependencies on other modules."; + uses deps-grp; + } + + leaf-list inverse-deps { + type module-ref; + description + "List of modules that depend on this module."; + } + + list rpc { + key "path"; + description + "Module RPC/actions."; + + leaf path { + type yang:xpath1.0; + description + "Path identifying the operation."; + } + + container in { + description + "Operation input dependencies."; + uses deps-grp; + } + + container out { + description + "Operation output dependencies."; + uses deps-grp; + } + } + + list notification { + key "path"; + description + "Module notifications."; + + leaf path { + type yang:xpath1.0; + description + "Path identifying the notification."; + } + + container deps { + description + "Notification dependencies."; + uses deps-grp; + } + } + } + } +} From dab8c9aac96063ccb8541d5d1795ee89b75faeee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= Date: Wed, 2 Apr 2025 11:50:24 -0700 Subject: [PATCH 47/57] build: link to libpcre2 explicitly 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 --- CMakeLists.txt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index c5fec452..4e009aa7 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. From 7519fcd35216072a6b1eebe2a79e19789be345a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= Date: Wed, 9 Apr 2025 15:35:37 +0200 Subject: [PATCH 48/57] CI: renamed project upstream Change-Id: I5447f243297fbfde7c364eb3919b00db239bd069 --- .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 b41c4904..7b92766f 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 d76975bc..8291e001 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 d06b646a..0867f077 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} From 1b12ea9885649d4719dea56f79fc6f4412d0c735 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= Date: Tue, 27 May 2025 20:14:41 +0200 Subject: [PATCH 49/57] build: bump CMake I'd like to use C++23, which is supported as a CXX_STANDARD since CMake 3.20. At the same time, GitHub CI is sunsetting the Ubuntu 20.04 builders this year, and Ubuntu 22.04 ships with CMake 3.22. Let's upgrade. Change-Id: I2252f5d6522938dfb78d3402326b240a30329cc8 --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 4e009aa7..0803876c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,7 +4,7 @@ # Written by Václav Kubernát # -cmake_minimum_required(VERSION 3.19) +cmake_minimum_required(VERSION 3.22) project(libyang-cpp LANGUAGES CXX) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) From 00f2a7d4e8d281ca29322112f813a549c3758f11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= Date: Tue, 27 May 2025 19:42:18 +0200 Subject: [PATCH 50/57] build: try building with C++23, if available We've been using the `CMAKE_CXX_STANDARD_REQUIRED ON` from the very beginning, at least from cla-sysrepo's commit c2e71252 back in 2016. I guess that the thinking that I followed was to make sure that if the compiler is too old, we will get a clean error at CMake time rather than a build error later on. However, this actually prevents us from "opportunistically" enabling support for C++23 *if available*. The way how CMake behaves [1] is to actually silently downgrade to older versions of the standard. And this is what we want today: use C++23 if available, and if not, use C++20. The only downside is that we'll get no "early error" when building on, say, a C++14 compiler, but I think I can live with that. [1] https://cmake.org/cmake/help/latest/prop_tgt/CXX_STANDARD.html Change-Id: Idc60365f9c1763ae12e2a871a4702e46237c156d --- CMakeLists.txt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 0803876c..05f5d45b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,8 +6,7 @@ cmake_minimum_required(VERSION 3.22) project(libyang-cpp LANGUAGES CXX) -set(CMAKE_CXX_STANDARD 20) -set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_STANDARD 23) include(CTest) include(GNUInstallDirs) From 0b4545062f32ae4f7d83a8b4efcdef1a7ef3e185 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= Date: Tue, 27 May 2025 19:51:12 +0200 Subject: [PATCH 51/57] disable building tests for hh-date on new enough GCC MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On GCC 14.2, this won't build for me: include/libyang-cpp/Time.hpp:117:34: error: no matching function for call to ‘parse(const char [21], std::chrono::time_point > >&)’ The error message is actually the same as in a proposed fix [1], and upstream maintainer has said that they actually recommend relying on the STL on this "new enough" platform. I'm very happy to oblige. We were discussing this last year (I51242fe2ec873fba5954c44e38e898d205a9e484), but at the time this was implemented the build failure was not known, AFAICT, so let's assume that the former use case for building with "new enough STL", but actually forcing HH-date, is no longer important. [1] https://github.com/HowardHinnant/date/pull/866 Change-Id: Id4b0206e2a960686fb1d8e76134b9061d7545d81 --- CMakeLists.txt | 5 ----- include/libyang-cpp/Time.hpp | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 05f5d45b..c6db3208 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -129,11 +129,6 @@ if(BUILD_TESTING) add_executable(test_time-stl-hhdate tests/time.cpp) target_link_libraries(test_time-stl-hhdate DoctestIntegration yang-cpp date::date-tz) add_test(test_time-stl-hhdate test_time-stl-hhdate) - - add_executable(test_time-hhdate tests/time.cpp) - target_link_libraries(test_time-hhdate DoctestIntegration yang-cpp date::date-tz) - target_compile_definitions(test_time-hhdate PUBLIC LIBYANG_CPP_SKIP_STD_CHRONO_TZ) - add_test(test_time-hhdate test_time-hhdate) endif() endif() diff --git a/include/libyang-cpp/Time.hpp b/include/libyang-cpp/Time.hpp index 36921f10..14bc201c 100644 --- a/include/libyang-cpp/Time.hpp +++ b/include/libyang-cpp/Time.hpp @@ -10,7 +10,7 @@ #include #include "libyang-cpp/export.h" -#if __cpp_lib_chrono >= 201907L && !LIBYANG_CPP_SKIP_STD_CHRONO_TZ +#if __cpp_lib_chrono >= 201907L namespace libyang { namespace date_impl = std::chrono; } From 249da7280864fbda5fccb340b455b7000ebfe67d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= Date: Wed, 28 May 2025 09:58:55 +0200 Subject: [PATCH 52/57] fix compilation with gcc15 From C++23, the literal operator for strings must not have a space between the quotes and the underscore [1]. GCC15 started to warn about this. [1] https://cplusplus.github.io/CWG/issues/2521.html Change-Id: Ie0519f0afc56d8762beb4009baeb4111f8290c89 --- include/libyang-cpp/Value.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/libyang-cpp/Value.hpp b/include/libyang-cpp/Value.hpp index f26f46e9..e721ba33 100644 --- a/include/libyang-cpp/Value.hpp +++ b/include/libyang-cpp/Value.hpp @@ -201,7 +201,7 @@ constexpr Decimal64 make_decimal64() } inline namespace literals { template -constexpr Decimal64 operator"" _decimal64() +constexpr Decimal64 operator""_decimal64() { return impl::make_decimal64<0, 0, 0, Cs...>(); } From 22b41e7ed8268b94ccc139138e2037c390a3a616 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bed=C5=99ich=20Schindler?= Date: Thu, 10 Jul 2025 15:00:45 +0200 Subject: [PATCH 53/57] Implement `SchemaNode::actionRpcs()` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- 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 0f1a4c42..9f49fd7c 100644 --- a/include/libyang-cpp/SchemaNode.hpp +++ b/include/libyang-cpp/SchemaNode.hpp @@ -79,6 +79,7 @@ class LIBYANG_CPP_EXPORT SchemaNode { Collection siblings() const; Collection immediateChildren() const; std::vector extensionInstances() const; + std::vector actionRpcs() const; std::vector when() const; diff --git a/src/SchemaNode.cpp b/src/SchemaNode.cpp index 26b5099f..ce242030 100644 --- a/src/SchemaNode.cpp +++ b/src/SchemaNode.cpp @@ -117,6 +117,18 @@ Collection SchemaNode::immediateChildren() c return c ? c->siblings() : Collection{nullptr, nullptr}; } +/** + * @brief Returns a collection of action nodes (not RPC nodes) as SchemaNode + */ +std::vector SchemaNode::actionRpcs() const +{ + std::vector res; + for (auto action = reinterpret_cast(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 0d8acb99..02d3d74c 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 00013771..65ef462c 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"); From 1f8e502cf0308faa0336b1077308184de6a83ee0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= Date: Mon, 20 Oct 2025 11:45:06 +0200 Subject: [PATCH 54/57] CI: pin to libyang v3 Change-Id: Idba085ad9c9c137874801f1d4089f8ee6bb390c7 --- .zuul.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.zuul.yaml b/.zuul.yaml index 7b92766f..fd89549b 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-08-07 - name: github/doctest/doctest override-checkout: v2.3.6 - f38-clang-asan-ubsan: required-projects: &projects - name: github/CESNET/libyang - override-checkout: devel + override-checkout: cesnet/2025-08-07 - name: github/doctest/doctest override-checkout: v2.4.11 - f38-clang-tsan: From b27a65d5d5f6ffffbd2c26c1b5bea57a6de584d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= Date: Mon, 20 Oct 2025 18:46:38 +0200 Subject: [PATCH 55/57] wrap lyd_validate_op This patch wraps lyd_validate_op from libyang. Downstream users can now validate operations (RPC input, replies, and notifications). Change-Id: Ib03070bbc3e1d0dccd6bf38eda82e944db1093b1 --- include/libyang-cpp/DataNode.hpp | 2 + src/DataNode.cpp | 24 +++++++++++ tests/context.cpp | 2 +- tests/data_node.cpp | 72 ++++++++++++++++++++++++++++++++ tests/example_schema.hpp | 22 ++++++++++ 5 files changed, 121 insertions(+), 1 deletion(-) diff --git a/include/libyang-cpp/DataNode.hpp b/include/libyang-cpp/DataNode.hpp index 50d6c0e7..e202a96f 100644 --- a/include/libyang-cpp/DataNode.hpp +++ b/include/libyang-cpp/DataNode.hpp @@ -59,6 +59,7 @@ template void handleLyTreeOperation(DataNode* affectedNode, Operation operation, Siblings siblings, std::shared_ptr newRefs); LIBYANG_CPP_EXPORT void validateAll(std::optional& node, const std::optional& opts = std::nullopt); +LIBYANG_CPP_EXPORT void validateOp(libyang::DataNode& input, const std::optional& opsTree, OperationType opType); LIBYANG_CPP_EXPORT Set findXPathAt( const std::optional& contextNode, @@ -147,6 +148,7 @@ class LIBYANG_CPP_EXPORT DataNode { friend LIBYANG_CPP_EXPORT lyd_node* getRawNode(DataNode node); friend LIBYANG_CPP_EXPORT void validateAll(std::optional& node, const std::optional& opts); + friend LIBYANG_CPP_EXPORT void validateOp(libyang::DataNode& input, const std::optional& opsTree, OperationType opType); friend LIBYANG_CPP_EXPORT Set findXPathAt(const std::optional& contextNode, const libyang::DataNode& forest, const std::string& xpath); bool operator==(const DataNode& node) const; diff --git a/src/DataNode.cpp b/src/DataNode.cpp index 7e87917a..e28ef308 100644 --- a/src/DataNode.cpp +++ b/src/DataNode.cpp @@ -1226,6 +1226,30 @@ void validateAll(std::optional& node, const std::optional& opsTree, OperationType opType) +{ + if (opType == OperationType::RpcYang || opType == OperationType::RpcRestconf || opType == OperationType::RpcNetconf) { + opType = OperationType::RpcYang; + } else if (opType == OperationType::ReplyYang || opType == OperationType::ReplyRestconf || opType == OperationType::ReplyNetconf) { + opType = OperationType::ReplyYang; + } else if (opType == OperationType::NotificationYang || opType == OperationType::NotificationRestconf || opType == OperationType::NotificationNetconf) { + opType = OperationType::NotificationYang; + } else { + throw Error("validateOp: DataYang datatype is not supported"); + } + + auto ret = lyd_validate_op(input.m_node, opsTree ? opsTree->m_node : nullptr, utils::toOpType(opType), nullptr); + throwIfError(ret, "libyang:validateOp: lyd_validate_op failed"); +} + /** @short Find instances matching the provided XPath * * @param contextNode The node which serves as the "context node" for XPath evaluation. Use nullopt to start at root. diff --git a/tests/context.cpp b/tests/context.cpp index 6c5dde83..c0b7e091 100644 --- a/tests/context.cpp +++ b/tests/context.cpp @@ -154,7 +154,7 @@ TEST_CASE("context") { auto mod = ctx->parseModule(example_schema, libyang::SchemaFormat::YANG); auto rpcs = mod.actionRpcs(); - REQUIRE(rpcs.size() == 1); + REQUIRE(rpcs.size() == 2); REQUIRE(rpcs[0].module().name() == "example-schema"); REQUIRE(rpcs[0].name() == "myRpc"); diff --git a/tests/data_node.cpp b/tests/data_node.cpp index db5a28eb..9215b122 100644 --- a/tests/data_node.cpp +++ b/tests/data_node.cpp @@ -2430,6 +2430,78 @@ TEST_CASE("Data Node manipulation") "Can't parse into operation data tree: LY_EVALID", libyang::Error); } } + + DOCTEST_SUBCASE("Validation") + { + DOCTEST_SUBCASE("Valid input") + { + std::string rpcInput; + std::string rpcPath; + std::string expected; + std::optional depTree; + + DOCTEST_SUBCASE("RPC") + { + rpcInput = R"({"example-schema:input": { "number": 42 } })"; + rpcPath = "/example-schema:rpc-with-choice"; + expected = R"({ + "example-schema:rpc-with-choice": { + "number": 42 + } +} +)"; + } + + DOCTEST_SUBCASE("Action") + { + rpcInput = R"({ "example-schema:input": { "friend": "Kuba" } })"; + rpcPath = "/example-schema:person[name='Franta']/poke-a-friend"; + expected = R"({ + "example-schema:poke-a-friend": { + "friend": "Kuba" + } +} +)"; + depTree = ctx.newPath("/example-schema:person[name='Kuba']"); + } + + auto [parent, rpcTree] = ctx.newPath2(rpcPath); + auto rpcOp = rpcTree->parseOp(rpcInput, dataTypeFor(rpcInput), libyang::OperationType::RpcRestconf); + + REQUIRE(!rpcOp.op); + REQUIRE(rpcOp.tree); + + libyang::validateOp(*rpcTree, depTree, libyang::OperationType::RpcRestconf); + REQUIRE(*rpcTree->printStr(libyang::DataFormat::JSON, libyang::PrintFlags::KeepEmptyCont) == expected); + } + + DOCTEST_SUBCASE("Nodes in disjunctive cases defined together") + { + auto rpcInput = R"({ "example-schema:input": { "number": 42, "text": "The ultimate answer" } })"; + + auto rpcTree = ctx.newPath("/example-schema:rpc-with-choice"); + auto rpcOp = rpcTree.parseOp(rpcInput, dataTypeFor(rpcInput), libyang::OperationType::RpcRestconf); + + REQUIRE(!rpcOp.op); + REQUIRE(rpcOp.tree); + + REQUIRE_THROWS_WITH_AS(libyang::validateOp(rpcTree, std::nullopt, libyang::OperationType::RpcRestconf), "libyang:validateOp: lyd_validate_op failed: LY_EVALID", libyang::Error); + } + + DOCTEST_SUBCASE("Action without the leafref node") + { + auto rpcInput = R"({ "example-schema:input": { "friend": "Kuba" } })"; + auto rpcPath = "/example-schema:person[name='Franta']/poke-a-friend"; + + auto [parent, rpcTree] = ctx.newPath2(rpcPath); + auto rpcOp = rpcTree->parseOp(rpcInput, dataTypeFor(rpcInput), libyang::OperationType::RpcRestconf); + + REQUIRE(!rpcOp.op); + REQUIRE(rpcOp.tree); + + REQUIRE_THROWS_WITH_AS(libyang::validateOp(*rpcTree, std::nullopt, libyang::OperationType::RpcRestconf), "libyang:validateOp: lyd_validate_op failed: LY_EVALID", libyang::Error); + } + } } DOCTEST_SUBCASE("comparing") { diff --git a/tests/example_schema.hpp b/tests/example_schema.hpp index 02d3d74c..acc6eccd 100644 --- a/tests/example_schema.hpp +++ b/tests/example_schema.hpp @@ -108,6 +108,16 @@ module example-schema { } action poke { } + + action poke-a-friend { + input { + leaf friend { + type leafref { + path '../../../person/name'; + } + } + } + } } leaf bossPerson { @@ -284,6 +294,18 @@ module example-schema { } } } + rpc rpc-with-choice { + input { + choice the-impossible-choice { + leaf text { + type string; + } + leaf number { + type int32; + } + } + } + } anydata myData { } From 4c00c24dee6bbc694d62e7174b7ac14f8aa95f14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Kundr=C3=A1t?= Date: Thu, 30 Oct 2025 21:16:13 +0100 Subject: [PATCH 56/57] port to libyang v4 Apart from the usual API/ABI changes, the default values of a `leaf` or a `leaf-list` are no longer canonicalized. This used to be missing from libyang v4, but it was added recently. Sill, chances are that the form which was used in the YANG model's source code is actually *the* intended presentation, so let's skip canonicalization altogether. Upstream has removed some flags and defines, so let's sync their list with whatever is currently available in the `devel` branch. Change-Id: I52dcec88eeee003b45dd6cf400f4d6875abbcc05 --- .zuul.yaml | 4 ++-- CMakeLists.txt | 4 ++-- README.md | 2 +- include/libyang-cpp/Context.hpp | 6 +++++- include/libyang-cpp/DataNode.hpp | 5 ++++- include/libyang-cpp/Enum.hpp | 15 +++++++++++++-- src/Context.cpp | 12 ++++++++++-- src/DataNode.cpp | 11 +++++++++-- src/SchemaNode.cpp | 6 +++--- src/Type.cpp | 2 +- src/utils/enum.hpp | 21 +++++++++++++++++++-- tests/context.cpp | 32 +++++++++++++++++--------------- tests/data_node.cpp | 2 +- tests/schema_node.cpp | 4 ++-- 14 files changed, 89 insertions(+), 37 deletions(-) diff --git a/.zuul.yaml b/.zuul.yaml index fd89549b..7b92766f 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -4,13 +4,13 @@ - f38-gcc-cover: required-projects: - name: github/CESNET/libyang - override-checkout: cesnet/2025-08-07 + override-checkout: devel - name: github/doctest/doctest override-checkout: v2.3.6 - f38-clang-asan-ubsan: required-projects: &projects - name: github/CESNET/libyang - override-checkout: cesnet/2025-08-07 + override-checkout: devel - name: github/doctest/doctest override-checkout: v2.4.11 - f38-clang-tsan: diff --git a/CMakeLists.txt b/CMakeLists.txt index c6db3208..90f9aaa4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -20,7 +20,7 @@ add_custom_target(libyang-cpp-version-cmake cmake/ProjectGitVersionRunner.cmake ) include(cmake/ProjectGitVersion.cmake) -set(LIBYANG_CPP_PKG_VERSION "4") +set(LIBYANG_CPP_PKG_VERSION "5") prepare_git_version(LIBYANG_CPP_VERSION ${LIBYANG_CPP_PKG_VERSION}) find_package(Doxygen) @@ -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.10.1 IMPORTED_TARGET) +pkg_check_modules(LIBYANG REQUIRED libyang>=4.1.0 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/README.md b/README.md index 8291e001..0cef2ffe 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Object lifetimes are managed automatically via RAII. ## Dependencies -- [libyang v3](https://github.com/CESNET/libyang) - the `devel` branch (even for the `master` branch of *libyang-cpp*) +- [libyang v4](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/doctest/doctest/) as a C++ unit test framework diff --git a/include/libyang-cpp/Context.hpp b/include/libyang-cpp/Context.hpp index ca890632..e6959141 100644 --- a/include/libyang-cpp/Context.hpp +++ b/include/libyang-cpp/Context.hpp @@ -108,7 +108,11 @@ class LIBYANG_CPP_EXPORT Context { std::optional getSubmodule(const std::string& name, const std::optional& revision) const; void registerModuleCallback(std::function callback); - ParsedOp parseOp(const std::string& input, const DataFormat format, const OperationType opType) const; + ParsedOp parseOp( + const std::string& input, + const DataFormat format, + const OperationType opType, + const std::optional parseOpts = std::nullopt) const; DataNode newPath(const std::string& path, const std::optional& value = std::nullopt, const std::optional options = std::nullopt) const; CreatedNodes newPath2(const std::string& path, const std::optional& value = std::nullopt, const std::optional options = std::nullopt) const; diff --git a/include/libyang-cpp/DataNode.hpp b/include/libyang-cpp/DataNode.hpp index e202a96f..35e5936c 100644 --- a/include/libyang-cpp/DataNode.hpp +++ b/include/libyang-cpp/DataNode.hpp @@ -123,7 +123,10 @@ class LIBYANG_CPP_EXPORT DataNode { Collection siblings() const; Collection immediateChildren() const; - ParsedOp parseOp(const std::string& input, const DataFormat format, const OperationType opType) const; + ParsedOp parseOp(const std::string& input, + const DataFormat format, + const OperationType opType, + const std::optional parseOpts = std::nullopt) const; void parseSubtree( const std::string& data, diff --git a/include/libyang-cpp/Enum.hpp b/include/libyang-cpp/Enum.hpp index 85dcc838..32142f95 100644 --- a/include/libyang-cpp/Enum.hpp +++ b/include/libyang-cpp/Enum.hpp @@ -136,6 +136,7 @@ enum class DuplicationOptions : uint32_t { WithFlags = 0x08, NoExt = 0x10, WithPriv = 0x20, + NoLyds = 0x40, }; enum class NodeType : uint16_t { @@ -167,6 +168,13 @@ enum class ContextOptions : uint16_t { PreferSearchDirs = 0x20, SetPrivParsed = 0x40, ExplicitCompile = 0x80, + EnableImpFeatures = 0x100, + CompileObsolete = 0x200, + LybHashes = 0x400, + LeafrefExtended = 0x800, + LeafrefLinking = 0x1000, + BuiltinPluginsOnly = 0x2000, + StaticPluginsOnly = 0x4000, }; /** @@ -203,7 +211,6 @@ enum class AnydataValueType : uint32_t { String, XML, JSON, - LYB }; /** @@ -252,6 +259,7 @@ enum class ValidationOptions { MultiError = 0x0004, Operational = 0x0008, NoDefaults = 0x0010, + NotFinal = 0x0020, }; /** @@ -262,11 +270,14 @@ enum class ParseOptions { Strict = 0x020000, Opaque = 0x040000, NoState = 0x080000, - LybModUpdate = 0x100000, + LybSkipCtxCheck = 0x100000, Ordered = 0x200000, Subtree = 0x400000, /**< Do not use this one for parsing of data subtrees */ WhenTrue = 0x800000, NoNew = 0x1000000, + StoreOnly = 0x2010000, + JsonNull = 0x4000000, + JsonStringDataTypes = 0x8000000, }; /** diff --git a/src/Context.cpp b/src/Context.cpp index fec2f271..f5bdc714 100644 --- a/src/Context.cpp +++ b/src/Context.cpp @@ -239,7 +239,7 @@ std::optional Context::parseExtData( * Note: to parse a NETCONF RPC reply, you MUST parse the original NETCONF RPC request (that is, you have to use * this method with OperationType::RpcNetconf). */ -ParsedOp Context::parseOp(const std::string& input, const DataFormat format, const OperationType opType) const +ParsedOp Context::parseOp(const std::string& input, const DataFormat format, const OperationType opType, const std::optional parseOpts) const { auto in = wrap_ly_in_new_memory(input); @@ -251,7 +251,15 @@ ParsedOp Context::parseOp(const std::string& input, const DataFormat format, con case OperationType::NotificationYang: { lyd_node* op = nullptr; lyd_node* tree = nullptr; - auto err = lyd_parse_op(m_ctx.get(), nullptr, in.get(), utils::toLydFormat(format), utils::toOpType(opType), &tree, &op); + auto err = lyd_parse_op( + m_ctx.get(), + nullptr, + in.get(), + utils::toLydFormat(format), + utils::toOpType(opType), + parseOpts ? utils::toParseOptions(*parseOpts) : 0, + &tree, + &op); ParsedOp res; res.tree = tree ? std::optional{libyang::wrapRawNode(tree)} : std::nullopt; diff --git a/src/DataNode.cpp b/src/DataNode.cpp index e28ef308..edabf9b1 100644 --- a/src/DataNode.cpp +++ b/src/DataNode.cpp @@ -391,7 +391,7 @@ DataNodeAny DataNode::asAny() const * * Wraps `lyd_parse_op`. */ -ParsedOp DataNode::parseOp(const std::string& input, const DataFormat format, const OperationType opType) const +ParsedOp DataNode::parseOp(const std::string& input, const DataFormat format, const OperationType opType, const std::optional parseOpts) const { auto in = wrap_ly_in_new_memory(input); @@ -401,7 +401,14 @@ ParsedOp DataNode::parseOp(const std::string& input, const DataFormat format, co case OperationType::ReplyRestconf: { lyd_node* op = nullptr; lyd_node* tree = nullptr; - auto err = lyd_parse_op(m_node->schema->module->ctx, m_node, in.get(), utils::toLydFormat(format), utils::toOpType(opType), &tree, nullptr); + auto err = lyd_parse_op(m_node->schema->module->ctx, + m_node, + in.get(), + utils::toLydFormat(format), + utils::toOpType(opType), + parseOpts ? utils::toParseOptions(*parseOpts) : 0, + &tree, + nullptr); ParsedOp res{ .tree = tree ? std::optional{libyang::wrapRawNode(tree)} : std::nullopt, .op = op ? std::optional{libyang::wrapRawNode(op)} : std::nullopt diff --git a/src/SchemaNode.cpp b/src/SchemaNode.cpp index ce242030..2e0351a0 100644 --- a/src/SchemaNode.cpp +++ b/src/SchemaNode.cpp @@ -564,7 +564,7 @@ std::vector LeafList::defaultValuesStr() const auto dflts = reinterpret_cast(m_node)->dflts; std::vector res; for (const auto& it : std::span(dflts, LY_ARRAY_COUNT(dflts))) { - res.emplace_back(lyd_value_get_canonical(m_ctx.get(), it)); + res.emplace_back(it.str); } return res; } @@ -660,8 +660,8 @@ bool LeafList::isUserOrdered() const std::optional Leaf::defaultValueStr() const { auto dflt = reinterpret_cast(m_node)->dflt; - if (dflt) { - return lyd_value_get_canonical(m_ctx.get(), dflt); + if (dflt.str) { + return std::string{dflt.str}; } else { return std::nullopt; } diff --git a/src/Type.cpp b/src/Type.cpp index 01b2c4f2..4d2db462 100644 --- a/src/Type.cpp +++ b/src/Type.cpp @@ -240,7 +240,7 @@ std::optional Type::description() const */ std::string Type::internalPluginId() const { - return m_type->plugin->id; + return lysc_get_type_plugin(m_type->plugin_ref)->id; } /** diff --git a/src/utils/enum.hpp b/src/utils/enum.hpp index 5e02d241..0b2e2d70 100644 --- a/src/utils/enum.hpp +++ b/src/utils/enum.hpp @@ -91,6 +91,7 @@ static_assert(LYD_DUP_WITH_FLAGS == toDuplicationOptions(DuplicationOptions::Wit static_assert(LYD_DUP_WITH_PARENTS == toDuplicationOptions(DuplicationOptions::WithParents)); static_assert(LYD_DUP_NO_EXT == toDuplicationOptions(DuplicationOptions::NoExt)); static_assert(LYD_DUP_WITH_PRIV == toDuplicationOptions(DuplicationOptions::WithPriv)); +static_assert(LYD_DUP_NO_LYDS == toDuplicationOptions(DuplicationOptions::NoLyds)); static_assert((LYD_DUP_NO_META | LYD_DUP_NO_EXT) == toDuplicationOptions(DuplicationOptions::NoMeta | DuplicationOptions::NoExt)); @@ -131,6 +132,13 @@ static_assert(toContextOptions(ContextOptions::DisableSearchCwd) == LY_CTX_DISAB static_assert(toContextOptions(ContextOptions::PreferSearchDirs) == LY_CTX_PREFER_SEARCHDIRS); static_assert(toContextOptions(ContextOptions::SetPrivParsed) == LY_CTX_SET_PRIV_PARSED); static_assert(toContextOptions(ContextOptions::ExplicitCompile) == LY_CTX_EXPLICIT_COMPILE); +static_assert(toContextOptions(ContextOptions::EnableImpFeatures) == LY_CTX_ENABLE_IMP_FEATURES); +static_assert(toContextOptions(ContextOptions::CompileObsolete) == LY_CTX_COMPILE_OBSOLETE); +static_assert(toContextOptions(ContextOptions::LybHashes) == LY_CTX_LYB_HASHES); +static_assert(toContextOptions(ContextOptions::LeafrefExtended) == LY_CTX_LEAFREF_EXTENDED); +static_assert(toContextOptions(ContextOptions::LeafrefLinking) == LY_CTX_LEAFREF_LINKING); +static_assert(toContextOptions(ContextOptions::BuiltinPluginsOnly) == LY_CTX_BUILTIN_PLUGINS_ONLY); +static_assert(toContextOptions(ContextOptions::StaticPluginsOnly) == LY_CTX_STATIC_PLUGINS_ONLY); constexpr uint16_t toLogOptions(const LogOptions options) { @@ -198,6 +206,10 @@ constexpr uint32_t toValidationOptions(const ValidationOptions opts) static_assert(toValidationOptions(ValidationOptions::NoState) == LYD_VALIDATE_NO_STATE); static_assert(toValidationOptions(ValidationOptions::Present) == LYD_VALIDATE_PRESENT); +static_assert(toValidationOptions(ValidationOptions::MultiError) == LYD_VALIDATE_MULTI_ERROR); +static_assert(toValidationOptions(ValidationOptions::Operational) == LYD_VALIDATE_OPERATIONAL); +static_assert(toValidationOptions(ValidationOptions::NoDefaults) == LYD_VALIDATE_NO_DEFAULTS); +static_assert(toValidationOptions(ValidationOptions::NotFinal) == LYD_VALIDATE_NOT_FINAL); constexpr uint32_t toParseOptions(const ParseOptions opts) { @@ -208,8 +220,14 @@ static_assert(toParseOptions(ParseOptions::ParseOnly) == LYD_PARSE_ONLY); static_assert(toParseOptions(ParseOptions::Strict) == LYD_PARSE_STRICT); static_assert(toParseOptions(ParseOptions::Opaque) == LYD_PARSE_OPAQ); static_assert(toParseOptions(ParseOptions::NoState) == LYD_PARSE_NO_STATE); -static_assert(toParseOptions(ParseOptions::LybModUpdate) == LYD_PARSE_LYB_MOD_UPDATE); +static_assert(toParseOptions(ParseOptions::LybSkipCtxCheck) == LYD_PARSE_LYB_SKIP_CTX_CHECK); static_assert(toParseOptions(ParseOptions::Ordered) == LYD_PARSE_ORDERED); +static_assert(toParseOptions(ParseOptions::Subtree) == LYD_PARSE_SUBTREE); +static_assert(toParseOptions(ParseOptions::WhenTrue) == LYD_PARSE_WHEN_TRUE); +static_assert(toParseOptions(ParseOptions::NoNew) == LYD_PARSE_NO_NEW); +static_assert(toParseOptions(ParseOptions::StoreOnly) == LYD_PARSE_STORE_ONLY); +static_assert(toParseOptions(ParseOptions::JsonNull) == LYD_PARSE_JSON_NULL); +static_assert(toParseOptions(ParseOptions::JsonStringDataTypes) == LYD_PARSE_JSON_STRING_DATATYPES); constexpr lyd_type toOpType(const OperationType type) { @@ -242,7 +260,6 @@ static_assert(toAnydataValueType(AnydataValueType::DataTree) == LYD_ANYDATA_DATA static_assert(toAnydataValueType(AnydataValueType::String) == LYD_ANYDATA_STRING); static_assert(toAnydataValueType(AnydataValueType::XML) == LYD_ANYDATA_XML); static_assert(toAnydataValueType(AnydataValueType::JSON) == LYD_ANYDATA_JSON); -static_assert(toAnydataValueType(AnydataValueType::LYB) == LYD_ANYDATA_LYB); constexpr LYS_OUTFORMAT toLysOutFormat(const SchemaOutputFormat format) { diff --git a/tests/context.cpp b/tests/context.cpp index c0b7e091..eaedebf5 100644 --- a/tests/context.cpp +++ b/tests/context.cpp @@ -359,25 +359,27 @@ TEST_CASE("context") ctx->loadModule("mod1", std::nullopt, {}); ctx->parseModule(valid_yang_model, libyang::SchemaFormat::YANG); auto modules = ctx->modules(); - REQUIRE(modules.size() == 8); + REQUIRE(modules.size() == 9); REQUIRE(modules.at(0).name() == "ietf-yang-metadata"); REQUIRE(modules.at(0).ns() == "urn:ietf:params:xml:ns:yang:ietf-yang-metadata"); REQUIRE(modules.at(1).name() == "yang"); REQUIRE(modules.at(1).ns() == "urn:ietf:params:xml:ns:yang:1"); - REQUIRE(modules.at(2).name() == "ietf-inet-types"); - REQUIRE(modules.at(2).ns() == "urn:ietf:params:xml:ns:yang:ietf-inet-types"); - REQUIRE(modules.at(3).name() == "ietf-yang-types"); - REQUIRE(modules.at(3).ns() == "urn:ietf:params:xml:ns:yang:ietf-yang-types"); - REQUIRE(modules.at(4).name() == "ietf-yang-schema-mount"); - REQUIRE(modules.at(4).ns() == "urn:ietf:params:xml:ns:yang:ietf-yang-schema-mount"); - REQUIRE(modules.at(5).name() == "ietf-yang-structure-ext"); - REQUIRE(modules.at(5).ns() == "urn:ietf:params:xml:ns:yang:ietf-yang-structure-ext"); - REQUIRE(modules.at(6).name() == "mod1"); - REQUIRE(modules.at(6).ns() == "http://example.com"); - REQUIRE(*modules.at(6).revision() == "2021-11-15"); - REQUIRE(modules.at(7).name() == "test"); + REQUIRE(modules.at(2).name() == "default"); + REQUIRE(modules.at(2).ns() == "urn:ietf:params:xml:ns:netconf:default:1.0"); + REQUIRE(modules.at(3).name() == "ietf-inet-types"); + REQUIRE(modules.at(3).ns() == "urn:ietf:params:xml:ns:yang:ietf-inet-types"); + REQUIRE(modules.at(4).name() == "ietf-yang-types"); + REQUIRE(modules.at(4).ns() == "urn:ietf:params:xml:ns:yang:ietf-yang-types"); + REQUIRE(modules.at(5).name() == "ietf-yang-schema-mount"); + REQUIRE(modules.at(5).ns() == "urn:ietf:params:xml:ns:yang:ietf-yang-schema-mount"); + REQUIRE(modules.at(6).name() == "ietf-yang-structure-ext"); + REQUIRE(modules.at(6).ns() == "urn:ietf:params:xml:ns:yang:ietf-yang-structure-ext"); + REQUIRE(modules.at(7).name() == "mod1"); REQUIRE(modules.at(7).ns() == "http://example.com"); - REQUIRE(modules.at(7).revision() == std::nullopt); + REQUIRE(*modules.at(7).revision() == "2021-11-15"); + REQUIRE(modules.at(8).name() == "test"); + REQUIRE(modules.at(8).ns() == "http://example.com"); + REQUIRE(modules.at(8).revision() == std::nullopt); } DOCTEST_SUBCASE("Module comparison") @@ -704,7 +706,7 @@ TEST_CASE("context") DOCTEST_SUBCASE("schema printing") { - std::optional ctx_pp{std::in_place, std::nullopt, libyang::ContextOptions::NoYangLibrary | libyang::ContextOptions::DisableSearchCwd | libyang::ContextOptions::SetPrivParsed}; + std::optional ctx_pp{std::in_place, std::nullopt, libyang::ContextOptions::NoYangLibrary | libyang::ContextOptions::DisableSearchCwd | libyang::ContextOptions::SetPrivParsed | libyang::ContextOptions::CompileObsolete}; auto mod = ctx_pp->parseModule(type_module, libyang::SchemaFormat::YANG); REQUIRE(mod.printStr(libyang::SchemaOutputFormat::Tree) == R"(module: type_module diff --git a/tests/data_node.cpp b/tests/data_node.cpp index 9215b122..88ca5b9e 100644 --- a/tests/data_node.cpp +++ b/tests/data_node.cpp @@ -2426,7 +2426,7 @@ TEST_CASE("Data Node manipulation") "WTF": "foo bar baz" } } - )", libyang::DataFormat::JSON, libyang::OperationType::RpcRestconf), + )", libyang::DataFormat::JSON, libyang::OperationType::RpcRestconf, libyang::ParseOptions::Strict), "Can't parse into operation data tree: LY_EVALID", libyang::Error); } } diff --git a/tests/schema_node.cpp b/tests/schema_node.cpp index 65ef462c..58152a45 100644 --- a/tests/schema_node.cpp +++ b/tests/schema_node.cpp @@ -19,7 +19,7 @@ using namespace std::string_literals; TEST_CASE("SchemaNode") { std::optional ctx{std::in_place, std::nullopt, - libyang::ContextOptions::NoYangLibrary | libyang::ContextOptions::DisableSearchCwd}; + libyang::ContextOptions::NoYangLibrary | libyang::ContextOptions::DisableSearchCwd | libyang::ContextOptions::CompileObsolete}; std::optional ctxWithParsed{std::in_place, std::nullopt, libyang::ContextOptions::SetPrivParsed | libyang::ContextOptions::NoYangLibrary | libyang::ContextOptions::DisableSearchCwd}; ctx->parseModule(example_schema, libyang::SchemaFormat::YANG); @@ -841,7 +841,7 @@ TEST_CASE("SchemaNode") DOCTEST_SUBCASE("LeafList::defaultValuesStr") { - REQUIRE(ctx->findPath("/type_module:leafListWithDefault").asLeafList().defaultValuesStr() == std::vector{"-1", "512", "1024", "2048"}); + REQUIRE(ctx->findPath("/type_module:leafListWithDefault").asLeafList().defaultValuesStr() == std::vector{"-1", "+512", "0x400", "04000"}); REQUIRE(ctx->findPath("/type_module:leafListBasic").asLeafList().defaultValuesStr().size() == 0); } From 51d7457bd281e34bac92ff8231978766c7784657 Mon Sep 17 00:00:00 2001 From: Martin Bohal Date: Tue, 4 Nov 2025 14:26:05 +0100 Subject: [PATCH 57/57] Add SchemaNode::isOutput() method This commit introduces the `isOutput()` method to the `SchemaNode` class, which checks whether the schema node is part of an output statement subtree. It complements an already existing `isInput()` method. Change-Id: I8fe75f2e607c0159d8295d685a59d301155ca92e --- include/libyang-cpp/SchemaNode.hpp | 1 + src/SchemaNode.cpp | 10 ++++++++++ tests/schema_node.cpp | 7 +++++++ 3 files changed, 18 insertions(+) diff --git a/include/libyang-cpp/SchemaNode.hpp b/include/libyang-cpp/SchemaNode.hpp index 9f49fd7c..db5a63ec 100644 --- a/include/libyang-cpp/SchemaNode.hpp +++ b/include/libyang-cpp/SchemaNode.hpp @@ -58,6 +58,7 @@ class LIBYANG_CPP_EXPORT SchemaNode { Status status() const; Config config() const; bool isInput() const; + bool isOutput() const; NodeType nodeType() const; // It is possible to cast SchemaNode to another type via the following methods. The types are children classes of // SchemaNode. No problems with slicing can occur, because these types are value-based and aren't constructible diff --git a/src/SchemaNode.cpp b/src/SchemaNode.cpp index 2e0351a0..9241b23e 100644 --- a/src/SchemaNode.cpp +++ b/src/SchemaNode.cpp @@ -193,6 +193,16 @@ bool SchemaNode::isInput() const return m_node->flags & LYS_INPUT; } +/** + * @brief Checks whether this node is inside a subtree of an output statement. + * + * Wraps `LYS_OUTPUT`. + */ +bool SchemaNode::isOutput() const +{ + return m_node->flags & LYS_OUTPUT; +} + /** * Returns the node type of this node (e.g. leaf, container...). * diff --git a/tests/schema_node.cpp b/tests/schema_node.cpp index 58152a45..01c2f19d 100644 --- a/tests/schema_node.cpp +++ b/tests/schema_node.cpp @@ -135,6 +135,13 @@ TEST_CASE("SchemaNode") REQUIRE(!ctx->findPath("/type_module:leafString").isInput()); } + DOCTEST_SUBCASE("SchemaNode::isOutput") + { + REQUIRE(ctx->findPath("/example-schema:myRpc/outputLeaf", libyang::InputOutputNodes::Output).isOutput()); + REQUIRE(!ctx->findPath("/example-schema:myRpc/inputLeaf").isOutput()); + REQUIRE(!ctx->findPath("/type_module:leafString").isOutput()); + } + DOCTEST_SUBCASE("SchemaNode::module") { REQUIRE(ctx->findPath("/type_module:leafString").module().name() == "type_module");