diff --git a/include/sdf/InterfaceElements.hh b/include/sdf/InterfaceElements.hh index e99bfbb83..38262bd6a 100644 --- a/include/sdf/InterfaceElements.hh +++ b/include/sdf/InterfaceElements.hh @@ -95,6 +95,19 @@ class SDFORMAT_VISIBLE NestedInclude /// \param[in] _localModelName The local name public: void SetLocalModelName(const std::string &_localModelName); + /// \brief Namespace relative to immediate parent as specified in + /// `//include/namespace`. This is nullopt if `//include/namespace` is not + /// set. Then the namespace of the model must be determined by the custom + /// model parser from the included model file. + /// Example: `my_new_model_namespace` + /// \return The local namespace. nullopt if `//include/namespace` is not set + public: const std::optional &LocalModelNamespace() const; + + /// \brief Set the namespace relative to immediate parent as specified in + /// `//include/namespace` + /// \param[in] _localModelNs The local namespace + public: void SetLocalModelNamespace(const std::string &_localModelNs); + /// \brief Whether the model is static as defined by `//include/static`. This /// is nullopt if `//include/static` is not set. /// \return Whether the model is static. nullopt if `//include/static` is not diff --git a/include/sdf/Model.hh b/include/sdf/Model.hh index 25687ec91..538642cdb 100644 --- a/include/sdf/Model.hh +++ b/include/sdf/Model.hh @@ -91,6 +91,21 @@ namespace sdf /// \param[in] _name Name of the model. public: void SetName(const std::string &_name); + /// \brief Get the resolved namespace associated with the model. + /// \return Resolved namespace of the model if it has been set, + /// otherwise std::nullopt. + public: std::optional Namespace() const; + + /// \brief Get the raw namespace associated with the model. + /// \return Raw namespace of the model if it has been set, + /// otherwise std::nullopt. + public: std::optional RawNamespace() const; + + /// \brief Set the raw namespace associated with the model. + /// \param[in] _ns Raw namespace of the model. The `__name__` placeholder + /// will be replaced with the model name. + public: void SetRawNamespace(const std::string &_ns); + /// \brief Check if this model should be static. /// A static model is one that is not subject to physical forces (in other /// words, it's purely kinematic instead of dynamic). @@ -557,6 +572,14 @@ namespace sdf private: sdf::Frame PrepareForMerge(sdf::Errors &_errors, const std::string &_parentOfProxyFrame); + /// \brief Resolve namespace placeholders for this model. + /// \param[in] _rawNs The raw namespace string. + /// \param[in] _modelName The model name used to replace "__name__". + /// \return The namespace string with model placeholders resolved. + private: std::optional ResolveNamespace ( + const std::optional &_rawNs, + const std::string &_modelName); + /// \brief Allow Root::Load, World::SetPoseRelativeToGraph, or /// World::SetFrameAttachedToGraph to call SetPoseRelativeToGraph and /// SetFrameAttachedToGraph diff --git a/sdf/1.11/model.sdf b/sdf/1.11/model.sdf index f8e85cb48..1027db32d 100644 --- a/sdf/1.11/model.sdf +++ b/sdf/1.11/model.sdf @@ -11,6 +11,15 @@ + + + Optional namespace associated with the model. This namespace can be + used by downstream systems and applications to organize related entities + and communication interfaces. The `__name__` placeholder will be + replaced with the model name. + + + The name of the model's canonical link, to which the model's implicit @@ -69,6 +78,10 @@ Override the name of the included model. + + Override the namespace of the included model. + + Override the static value of the included model. diff --git a/sdf/1.11/world.sdf b/sdf/1.11/world.sdf index 6235e193b..62df59dde 100644 --- a/sdf/1.11/world.sdf +++ b/sdf/1.11/world.sdf @@ -40,6 +40,10 @@ Override the static value of the included entity. + + Override the namespace of the included model. + + diff --git a/sdf/1.12/model.sdf b/sdf/1.12/model.sdf index 087f892e7..2e8640a46 100644 --- a/sdf/1.12/model.sdf +++ b/sdf/1.12/model.sdf @@ -11,6 +11,15 @@ + + + Optional namespace associated with the model. This namespace can be + used by downstream systems and applications to organize related entities + and communication interfaces. The `__name__` placeholder will be + replaced with the model name. + + + The name of the model's canonical link, to which the model's implicit @@ -70,6 +79,10 @@ Override the name of the included model. + + Override the namespace of the included model. + + Override the static value of the included model. diff --git a/sdf/1.12/world.sdf b/sdf/1.12/world.sdf index d39ad8819..a62f23628 100644 --- a/sdf/1.12/world.sdf +++ b/sdf/1.12/world.sdf @@ -36,6 +36,10 @@ Override the name of the included entity. + + Override the namespace of the included model. + + Override the static value of the included entity. diff --git a/src/InterfaceElements.cc b/src/InterfaceElements.cc index e5a861538..7acaeb8e1 100644 --- a/src/InterfaceElements.cc +++ b/src/InterfaceElements.cc @@ -52,6 +52,13 @@ class sdf::NestedInclude::Implementation /// Example: `my_new_model` public: std::optional localModelName; + /// \brief Namespace relative to immediate parent as specified in + /// `//include/namespace`. This is nullopt if `//include/namespace` is not + /// set. Then the namespace of the model must be determined by the custom + /// model parser from the included model file. + /// Example: `my_new_model_namespace` + public: std::optional localModelNs; + /// \brief Whether the model is static as defined by `//include/static`. This /// is nullopt if `//include/static` is not set. public: std::optional isStatic; @@ -129,6 +136,18 @@ void NestedInclude::SetLocalModelName(const std::string &_localModelName) this->dataPtr->localModelName = _localModelName; } +///////////////////////////////////////////////// +const std::optional &NestedInclude::LocalModelNamespace() const +{ + return this->dataPtr->localModelNs; +} + +///////////////////////////////////////////////// +void NestedInclude::SetLocalModelNamespace(const std::string &_localModelNs) +{ + this->dataPtr->localModelNs = _localModelNs; +} + ///////////////////////////////////////////////// const std::optional &NestedInclude::IsStatic() const { diff --git a/src/Model.cc b/src/Model.cc index 944e50b87..616fb8d60 100644 --- a/src/Model.cc +++ b/src/Model.cc @@ -15,6 +15,7 @@ * */ #include +#include #include #include #include @@ -44,6 +45,12 @@ class sdf::Model::Implementation /// \brief Name of the model. public: std::string name = ""; + /// \brief The unresolved namespace specified by the user. + public: std::optional rawNamespace; + + /// \brief The namespace after model name placeholders have been resolved. + public: std::optional resolvedNamespace; + /// \brief True if this model is specified as static, false otherwise. public: bool isStatic = false; @@ -178,6 +185,15 @@ Errors Model::Load(sdf::ElementPtr _sdf, const ParserConfig &_config) "] is reserved."}); } + // Read the model's namespace + auto nsAttribute = _sdf->GetAttribute("namespace"); + if (nsAttribute && nsAttribute->GetSet()) + { + this->dataPtr->rawNamespace = _sdf->Get("namespace", "").first; + this->dataPtr->resolvedNamespace = + this->ResolveNamespace(this->dataPtr->rawNamespace, this->Name()); + } + // Read the model's canonical_link attribute if (_sdf->HasAttribute("canonical_link")) { @@ -534,6 +550,28 @@ std::string Model::Name() const void Model::SetName(const std::string &_name) { this->dataPtr->name = _name; + this->dataPtr->resolvedNamespace = + this->ResolveNamespace(this->dataPtr->rawNamespace, _name); +} + +///////////////////////////////////////////////// +std::optional Model::Namespace() const +{ + return this->dataPtr->resolvedNamespace; +} + +///////////////////////////////////////////////// +std::optional Model::RawNamespace() const +{ + return this->dataPtr->rawNamespace; +} + +///////////////////////////////////////////////// +void Model::SetRawNamespace(const std::string &_ns) +{ + this->dataPtr->rawNamespace = _ns; + this->dataPtr->resolvedNamespace = + this->ResolveNamespace(this->dataPtr->rawNamespace, this->Name()); } ///////////////////////////////////////////////// @@ -1094,6 +1132,11 @@ sdf::ElementPtr Model::ToElement(const OutputConfig &_config) const sdf::ElementPtr includeElem = worldElem->AddElement("include"); includeElem->GetElement("uri")->Set(this->Uri()); includeElem->GetElement("name")->Set(this->Name()); + const auto rawNamespace = this->RawNamespace(); + if (rawNamespace.has_value()) + { + includeElem->GetElement("namespace")->Set(rawNamespace.value()); + } includeElem->GetElement("pose")->Set(this->RawPose()); if (!this->dataPtr->poseRelativeTo.empty()) { @@ -1117,6 +1160,11 @@ sdf::ElementPtr Model::ToElement(const OutputConfig &_config) const sdf::ElementPtr elem(new sdf::Element); sdf::initFile("model.sdf", elem); elem->GetAttribute("name")->Set(this->Name()); + const auto rawNamespace = this->RawNamespace(); + if (rawNamespace.has_value()) + { + elem->GetAttribute("namespace")->Set(rawNamespace.value()); + } if (!this->dataPtr->canonicalLink.empty()) { @@ -1414,3 +1462,23 @@ sdf::Frame Model::PrepareForMerge(sdf::Errors &_errors, return proxyFrame; } + +std::optional Model::ResolveNamespace( + const std::optional &_rawNs, + const std::string &_modelName) +{ + if (_rawNs == std::nullopt) + { + return std::nullopt; + } + + std::optional resolved = _rawNs; + std::size_t pos = 0; + + while ((pos = resolved->find("__name__", pos)) != std::string::npos) + { + resolved->replace(pos, strlen("__name__"), _modelName); + pos += _modelName.size(); + } + return resolved; +} diff --git a/src/Model_TEST.cc b/src/Model_TEST.cc index cb4325d76..d9fdcfb32 100644 --- a/src/Model_TEST.cc +++ b/src/Model_TEST.cc @@ -31,9 +31,23 @@ TEST(DOMModel, Construction) sdf::Model model; EXPECT_EQ(nullptr, model.Element()); EXPECT_TRUE(model.Name().empty()); - + EXPECT_FALSE(model.Namespace().has_value()); + EXPECT_FALSE(model.RawNamespace().has_value()); + + model.SetName("test_name"); + EXPECT_EQ("test_name", model.Name()); + model.SetRawNamespace(""); + ASSERT_TRUE(model.Namespace().has_value()); + EXPECT_EQ("", model.Namespace().value()); + model.SetRawNamespace("test/__name__1/__name__2/test_ns"); + ASSERT_TRUE(model.Namespace().has_value()); + EXPECT_EQ("test/test_name1/test_name2/test_ns", model.Namespace().value()); model.SetName("test_model"); - EXPECT_EQ("test_model", model.Name()); + ASSERT_TRUE(model.Namespace().has_value()); + EXPECT_EQ("test/test_model1/test_model2/test_ns", model.Namespace().value()); + model.SetRawNamespace("test_namespace"); + ASSERT_TRUE(model.Namespace().has_value()); + EXPECT_EQ("test_namespace", model.Namespace().value()); EXPECT_FALSE(model.Static()); model.SetStatic(true); @@ -165,9 +179,12 @@ TEST(DOMModel, CopyConstructor) { sdf::Model model; model.SetName("test_model"); + model.SetRawNamespace("test_ns"); sdf::Model model2(model); EXPECT_EQ("test_model", model2.Name()); + ASSERT_TRUE(model2.Namespace().has_value()); + EXPECT_EQ("test_ns", model2.Namespace().value()); } ///////////////////////////////////////////////// @@ -175,10 +192,13 @@ TEST(DOMModel, CopyAssignmentOperator) { sdf::Model model; model.SetName("test_model"); + model.SetRawNamespace("test_ns"); sdf::Model model2; model2 = model; EXPECT_EQ("test_model", model2.Name()); + ASSERT_TRUE(model2.Namespace().has_value()); + EXPECT_EQ("test_ns", model2.Namespace().value()); } ///////////////////////////////////////////////// @@ -186,9 +206,12 @@ TEST(DOMModel, MoveConstructor) { sdf::Model model; model.SetName("test_model"); + model.SetRawNamespace("test_ns"); sdf::Model model2(std::move(model)); EXPECT_EQ("test_model", model2.Name()); + ASSERT_TRUE(model2.Namespace().has_value()); + EXPECT_EQ("test_ns", model2.Namespace().value()); } ///////////////////////////////////////////////// @@ -196,10 +219,13 @@ TEST(DOMModel, MoveAssignmentOperator) { sdf::Model model; model.SetName("test_model"); + model.SetRawNamespace("test_ns"); sdf::Model model2; model2 = std::move(model); EXPECT_EQ("test_model", model2.Name()); + ASSERT_TRUE(model2.Namespace().has_value()); + EXPECT_EQ("test_ns", model2.Namespace().value()); } ///////////////////////////////////////////////// @@ -207,9 +233,11 @@ TEST(DOMModel, CopyAssignmentAfterMove) { sdf::Model model1; model1.SetName("model1"); + model1.SetRawNamespace("test_ns1"); sdf::Model model2; model2.SetName("model2"); + model2.SetRawNamespace("test_ns2"); // This is similar to what std::swap does except it uses std::move for each // assignment @@ -219,6 +247,10 @@ TEST(DOMModel, CopyAssignmentAfterMove) EXPECT_EQ("model2", model1.Name()); EXPECT_EQ("model1", model2.Name()); + ASSERT_TRUE(model1.Namespace().has_value()); + EXPECT_EQ("test_ns2", model1.Namespace().value()); + ASSERT_TRUE(model2.Namespace().has_value()); + EXPECT_EQ("test_ns1", model2.Namespace().value()); } ///////////////////////////////////////////////// @@ -333,6 +365,7 @@ TEST(DOMModel, ToElement) sdf::Model model; model.SetName("my-model"); + model.SetRawNamespace("__name__/ns"); model.SetStatic(true); model.SetSelfCollide(true); model.SetAllowAutoDisable(true); @@ -402,6 +435,7 @@ TEST(DOMModel, ToElement) model2.Load(elem); EXPECT_EQ(model.Name(), model2.Name()); + EXPECT_EQ(model.Namespace(), model2.Namespace()); EXPECT_EQ(model.Static(), model2.Static()); EXPECT_EQ(model.SelfCollide(), model2.SelfCollide()); EXPECT_EQ(model.AllowAutoDisable(), model2.AllowAutoDisable()); @@ -443,11 +477,13 @@ TEST(DOMModel, Uri) { sdf::Model model; std::string name = "my-model"; + std::string ns = "__name__/ns"; gz::math::Pose3d pose(1, 2, 3, 0.1, 0.2, 0.3); std::string uri = "https://fuel.gazebosim.org/1.0/openrobotics/models/my-model"; model.SetName(name); + model.SetRawNamespace(ns); model.SetRawPose(pose); model.SetStatic(true); model.SetPlacementFrameName("link0"); @@ -468,6 +504,10 @@ TEST(DOMModel, Uri) ASSERT_NE(nullptr, nameElem); EXPECT_EQ(name, nameElem->Get()); + sdf::ElementPtr nsElem = elem->FindElement("namespace"); + ASSERT_NE(nullptr, nsElem); + EXPECT_EQ(ns, nsElem->Get()); + sdf::ElementPtr poseElem = elem->FindElement("pose"); ASSERT_NE(nullptr, poseElem); EXPECT_EQ(pose, poseElem->Get()); @@ -499,6 +539,10 @@ TEST(DOMModel, Uri) ASSERT_NE(nullptr, nameAttr); EXPECT_EQ(name, nameAttr->GetAsString()); + sdf::ParamPtr nsAttr = elem->GetAttribute("namespace"); + ASSERT_NE(nullptr, nsAttr); + EXPECT_EQ(ns, nsAttr->GetAsString()); + sdf::ParamPtr placementFrameAttr = elem->GetAttribute("placement_frame"); ASSERT_NE(nullptr, placementFrameAttr); EXPECT_EQ("link0", placementFrameAttr->GetAsString()); diff --git a/src/Utils.cc b/src/Utils.cc index e43632356..31084c041 100644 --- a/src/Utils.cc +++ b/src/Utils.cc @@ -249,6 +249,11 @@ sdf::Errors loadIncludedInterfaceModels(sdf::ElementPtr _sdf, { include.SetLocalModelName(includeElem->Get("name")); } + if (includeElem->HasElement("namespace")) + { + include.SetLocalModelNamespace( + includeElem->Get("namespace")); + } if (includeElem->HasElement("static")) { include.SetIsStatic(includeElem->Get("static")); diff --git a/src/parser.cc b/src/parser.cc index 992893987..65cd7b958 100644 --- a/src/parser.cc +++ b/src/parser.cc @@ -1831,6 +1831,17 @@ bool readXml(tinyxml2::XMLElement *_xml, ElementPtr _sdf, "[@name=\"" + overrideName + "\"]"); } + if (elemXml->FirstChildElement("namespace")) + { + const std::string overrideNamespace = + elemXml->FirstChildElement("namespace")->GetText(); + auto nsAttribute = topLevelElem->GetAttribute("namespace"); + if (nsAttribute) + { + nsAttribute->SetFromString(overrideNamespace); + } + } + tinyxml2::XMLElement *poseElemXml = elemXml->FirstChildElement("pose"); if (poseElemXml) diff --git a/test/integration/element_tracing.cc b/test/integration/element_tracing.cc index 443bf8ad0..6d92f5d77 100644 --- a/test/integration/element_tracing.cc +++ b/test/integration/element_tracing.cc @@ -209,7 +209,7 @@ TEST(ElementTracing, includes) ASSERT_NE(nullptr, overrideActorPluginElem); EXPECT_EQ(actorFilePath, overrideActorPluginElem->FilePath()); ASSERT_TRUE(overrideActorPluginElem->LineNumber().has_value()); - EXPECT_EQ(40, overrideActorPluginElem->LineNumber().value()); + EXPECT_EQ(42, overrideActorPluginElem->LineNumber().value()); EXPECT_EQ(overrideActorPluginXmlPath, overrideActorPluginElem->XmlPath()); // Lights @@ -270,7 +270,7 @@ TEST(ElementTracing, includes) ASSERT_NE(nullptr, overrideModelPluginElem); EXPECT_EQ(modelFilePath, overrideModelPluginElem->FilePath()); ASSERT_TRUE(overrideModelPluginElem->LineNumber().has_value()); - EXPECT_EQ(14, overrideModelPluginElem->LineNumber().value()); + EXPECT_EQ(15, overrideModelPluginElem->LineNumber().value()); EXPECT_EQ(overrideModelPluginXmlPath, overrideModelPluginElem->XmlPath()); #ifdef _WIN32 diff --git a/test/integration/includes.cc b/test/integration/includes.cc index adc80248e..bbb0c6dd2 100644 --- a/test/integration/includes.cc +++ b/test/integration/includes.cc @@ -155,6 +155,7 @@ TEST(IncludesTest, Includes) const sdf::Model *model = world->ModelByIndex(0); ASSERT_NE(nullptr, model); EXPECT_EQ("test_model", model->Name()); + EXPECT_FALSE(model->Namespace().has_value()); EXPECT_FALSE(model->Static()); EXPECT_EQ(1u, model->LinkCount()); ASSERT_FALSE(nullptr == model->LinkByIndex(0)); @@ -203,6 +204,8 @@ TEST(IncludesTest, Includes) const sdf::Model *model1 = world->ModelByIndex(1); ASSERT_NE(nullptr, model1); EXPECT_EQ("override_model_name", model1->Name()); + ASSERT_TRUE(model1->Namespace().has_value()); + EXPECT_EQ("override_model_namespace", *model1->Namespace()); EXPECT_TRUE(model1->Static()); EXPECT_EQ(gz::math::Pose3d(1, 2, 3, 0, 0, 0), model1->RawPose()); EXPECT_EQ("", model1->PoseRelativeTo()); @@ -212,6 +215,8 @@ TEST(IncludesTest, Includes) const sdf::Model *model2 = world->ModelByIndex(2); ASSERT_NE(nullptr, model2); EXPECT_EQ("test_model_with_file", model2->Name()); + ASSERT_TRUE(model2->Namespace().has_value()); + EXPECT_EQ("test_model_with_file", *model2->Namespace()); EXPECT_FALSE(model2->Static()); EXPECT_EQ(1u, model2->LinkCount()); ASSERT_NE(nullptr, model2->LinkByIndex(0)); @@ -260,6 +265,7 @@ TEST(IncludesTest, Includes_15) const sdf::Model *model = world->ModelByIndex(0); ASSERT_NE(nullptr, model); EXPECT_EQ("test_model", model->Name()); + EXPECT_FALSE(model->Namespace().has_value()); EXPECT_EQ(1u, model->LinkCount()); ASSERT_FALSE(nullptr == model->LinkByName("link")); @@ -310,6 +316,7 @@ TEST(IncludesTest, Includes_15_convert) sdf::ElementPtr modelElem = worldElem->GetElement("model"); ASSERT_NE(nullptr, modelElem); EXPECT_EQ(modelElem->Get("name"), "test_model"); + EXPECT_EQ(modelElem->Get("namespace"), ""); sdf::ElementPtr linkElem = modelElem->GetElement("link"); ASSERT_NE(nullptr, linkElem); diff --git a/test/integration/interface_api.cc b/test/integration/interface_api.cc index d255a0927..fb64457fa 100644 --- a/test/integration/interface_api.cc +++ b/test/integration/interface_api.cc @@ -170,6 +170,7 @@ TEST_F(InterfaceAPI, NestedIncludeData) file_wont_be_parsed.nonce_1 box + box_ns 1 0 0 0 0 0 value1 @@ -194,6 +195,7 @@ TEST_F(InterfaceAPI, NestedIncludeData) EXPECT_EQ(sdf::filesystem::append(this->modelDir, fileName), _include.ResolvedFileName()); EXPECT_EQ("box", *_include.LocalModelName()); + EXPECT_EQ("box_ns", *_include.LocalModelNamespace()); EXPECT_TRUE(_include.IsStatic().has_value()); EXPECT_TRUE(_include.IsStatic().value()); @@ -227,6 +229,7 @@ TEST_F(InterfaceAPI, NestedIncludeData) EXPECT_EQ(sdf::filesystem::append(modelDir, fileName), _include.ResolvedFileName()); EXPECT_FALSE(_include.LocalModelName().has_value()); + EXPECT_FALSE(_include.LocalModelNamespace().has_value()); EXPECT_FALSE(_include.IsStatic()); // Add error for test expectation later on. diff --git a/test/sdf/includes.sdf b/test/sdf/includes.sdf index fd75a8deb..06886d221 100644 --- a/test/sdf/includes.sdf +++ b/test/sdf/includes.sdf @@ -9,6 +9,7 @@ test_model override_model_name + override_model_namespace 1 2 3 0 0 0 true @@ -17,6 +18,7 @@ test_model/model.sdf test_model_with_file + __name__