From 9ccfbc19a1a969f4dd518c5fb8a8c34233990ba6 Mon Sep 17 00:00:00 2001 From: "Addisu Z. Taddese" Date: Tue, 28 Apr 2020 18:48:08 -0500 Subject: [PATCH 1/5] Enforce minimum/maximum values specified in SDFormat description files Signed-off-by: Addisu Z. Taddese --- include/sdf/Element.hh | 21 ++++- include/sdf/Param.hh | 64 ++++++++++++-- src/Element.cc | 24 ++++++ src/Param.cc | 120 +++++++++++++++++++++++++-- src/Param_TEST.cc | 20 +++++ src/parser.cc | 17 +++- src/parser_TEST.cc | 94 +++++++++++++++++++++ src/parser_private.hh | 30 +++++-- test/sdf/stricter_semantics_desc.sdf | 5 ++ 9 files changed, 372 insertions(+), 23 deletions(-) create mode 100644 test/sdf/stricter_semantics_desc.sdf diff --git a/include/sdf/Element.hh b/include/sdf/Element.hh index 528d4741a..214e3d2ae 100644 --- a/include/sdf/Element.hh +++ b/include/sdf/Element.hh @@ -174,15 +174,30 @@ namespace sdf const std::string &_description=""); /// \brief Add a value to this Element. - /// \param[in] _type Type of data the attribute will hold. - /// \param[in] _defaultValue Default value for the attribute. + /// \param[in] _type Type of data the parameter will hold. + /// \param[in] _defaultValue Default value for the parameter. /// \param[in] _required Requirement string. \as Element::SetRequired. - /// \param[in] _description A text description of the attribute. + /// \param[in] _description A text description of the parameter. /// \throws sdf::AssertionInternalError if an invalid type is given. public: void AddValue(const std::string &_type, const std::string &_defaultValue, bool _required, const std::string &_description=""); + /// \brief Add a value to this Element. This override allows passing min and + /// max values of the parameter. + /// \param[in] _type Type of data the parameter will hold. + /// \param[in] _defaultValue Default value for the parameter. + /// \param[in] _required Requirement string. \as Element::SetRequired. + /// \param[in] _minValue Minimum allowed value for the parameter. + /// \param[in] _maxValue Maximum allowed value for the parameter. + /// \param[in] _description A text description of the parameter. + /// \throws sdf::AssertionInternalError if an invalid type is given. + public: void AddValue(const std::string &_type, + const std::string &_defaultValue, bool _required, + const std::string &_minValue, + const std::string &_maxValue, + const std::string &_description = ""); + /// \brief Get the param of an attribute. /// \param[in] _key the name of the attribute. /// \return The parameter attribute value. NULL if the key is invalid. diff --git a/include/sdf/Param.hh b/include/sdf/Param.hh index 175ef521a..35bc51349 100644 --- a/include/sdf/Param.hh +++ b/include/sdf/Param.hh @@ -24,6 +24,7 @@ #include #include #include +#include #include #include #include @@ -105,6 +106,41 @@ namespace sdf const std::string &_default, bool _required, const std::string &_description = ""); + /// \brief Constructor with min and max values. + /// \param[in] _key Key for the parameter. + /// \param[in] _typeName String name for the value type (double, + /// int,...). + /// \param[in] _default Default value. + /// \param[in] _required True if the parameter is required to be set. + /// \param[in] _minValue Minimum allowed value for the parameter. + /// \param[in] _maxValue Maximum allowed value for the parameter. + /// \param[in] _description Description of the parameter. + /// \throws sdf::AssertionInternalError if an invalid type is given. + public: Param(const std::string &_key, const std::string &_typeName, + const std::string &_default, bool _required, + const std::string &_minValue, const std::string &_maxValue, + const std::string &_description = ""); + + /// \brief Copy constructor + /// Note that the updateFunc member does not get copied + /// \param[in] _param Param to copy + public: Param(const Param &_param); + + /// \brief Move constructor + /// \param[in] _param Param to move from + public: Param(Param &&_param) noexcept = default; + + /// \brief Copy assignment operator + /// Note that the updateFunc member will not get copied + /// \param[in] _param The parameter to set values from. + /// \return *This + public: Param &operator=(const Param &_param); + + /// \brief Move assignment operator + /// \param[in] _param Param to move from + /// \returns Reference to this + public: Param &operator=(Param &&_param) noexcept = default; + /// \brief Destructor public: virtual ~Param(); @@ -116,6 +152,18 @@ namespace sdf /// \return String containing the default value of the parameter. public: std::string GetDefaultAsString() const; + /// \brief Get the minimum allowed value as a string + /// \return Returns a string containing the minimum allowed value of the + /// parameter if the minimum value is specified in the SDFormat description + /// of the parameter. nullopt otherwise. + public: std::optional GetMinValueAsString() const; + + /// \brief Get the maximum allowed value as a string + /// \return Returns a string containing the maximum allowed value of the + /// parameter if the maximum value is specified in the SDFormat description + /// of the parameter. nullopt otherwise. + public: std::optional GetMaxValueAsString() const; + /// \brief Set the parameter value from a string. /// \param[in] _value New value for the parameter in string form. public: bool SetFromString(const std::string &_value); @@ -186,12 +234,6 @@ namespace sdf public: template bool GetDefault(T &_value) const; - /// \brief Equal operator. Set's the value and default value from the - /// provided Param. - /// \param[in] _param The parameter to set values from. - /// \return *This - public: Param &operator=(const Param &_param); - /// \brief Set the description of the parameter. /// \param[in] _desc New description for the parameter. public: void SetDescription(const std::string &_desc); @@ -200,6 +242,10 @@ namespace sdf /// \return The description of the parameter. public: std::string GetDescription() const; + /// \brief Validate the value against minimum and maximum allowed values + /// \return True if the value is valid + public: bool ValidateValue() const; + /// \brief Ostream operator. Outputs the parameter's value. /// \param[in] _out Output stream. /// \param[in] _p The parameter to output. @@ -258,6 +304,12 @@ namespace sdf /// \brief This parameter's default value public: ParamVariant defaultValue; + + /// \brief This parameter's minimum allowed value + public: std::optional minValue; + + /// \brief This parameter's maximum allowed value + public: std::optional maxValue; }; /////////////////////////////////////////////// diff --git a/src/Element.cc b/src/Element.cc index b0f924123..02854b37d 100644 --- a/src/Element.cc +++ b/src/Element.cc @@ -121,6 +121,19 @@ void Element::AddValue(const std::string &_type, _type, _defaultValue, _required, _description); } +///////////////////////////////////////////////// +void Element::AddValue(const std::string &_type, + const std::string &_defaultValue, + bool _required, + const std::string &_minValue, + const std::string &_maxValue, + const std::string &_description) +{ + this->dataPtr->value = + std::make_shared(this->dataPtr->name, _type, _defaultValue, + _required, _minValue, _maxValue, _description); +} + ///////////////////////////////////////////////// ParamPtr Element::CreateParam(const std::string &_key, const std::string &_type, @@ -251,6 +264,17 @@ void Element::PrintDescription(const std::string &_prefix) const << "'" << " default ='" << this->dataPtr->value->GetDefaultAsString() << "'"; + auto minValue = this->dataPtr->value->GetMinValueAsString(); + if (minValue.has_value()) + { + std::cout << " min ='" << *minValue << "'"; + } + + auto maxValue = this->dataPtr->value->GetMaxValueAsString(); + if (maxValue.has_value()) + { + std::cout << " max ='" << *maxValue << "'"; + } } std::cout << ">\n"; diff --git a/src/Param.cc b/src/Param.cc index 17fbb5f0b..10357cec0 100644 --- a/src/Param.cc +++ b/src/Param.cc @@ -72,6 +72,42 @@ Param::Param(const std::string &_key, const std::string &_typeName, this->dataPtr->defaultValue = this->dataPtr->value; } +////////////////////////////////////////////////// +Param::Param(const std::string &_key, const std::string &_typeName, + const std::string &_default, bool _required, + const std::string &_minValue, const std::string &_maxValue, + const std::string &_description) + : Param(_key, _typeName, _default, _required, _description) +{ + auto valCopy = this->dataPtr->value; + if (!_minValue.empty()) + { + SDF_ASSERT( + this->ValueFromString(_minValue), + std::string("Invalid [min] parameter in SDFormat description of [") + + _key + "]"); + this->dataPtr->minValue = this->dataPtr->value; + } + + if (!_maxValue.empty()) + { + SDF_ASSERT( + this->ValueFromString(_maxValue), + std::string("Invalid [max] parameter in SDFormat description of [") + + _key + "]"); + this->dataPtr->maxValue = this->dataPtr->value; + } + + this->dataPtr->value = valCopy; +} + +Param::Param(const Param &_param) + : dataPtr(std::make_unique(*_param.dataPtr)) +{ + // We don't want to copy the updateFunc + this->dataPtr->updateFunc = nullptr; +} + ////////////////////////////////////////////////// Param::~Param() { @@ -80,9 +116,11 @@ Param::~Param() ///////////////////////////////////////////////// Param &Param::operator=(const Param &_param) { - this->dataPtr->value = _param.dataPtr->value; - this->dataPtr->defaultValue = _param.dataPtr->defaultValue; - this->dataPtr->set = _param.dataPtr->set; + auto updateFuncCopy = this->dataPtr->updateFunc; + *this = Param(_param); + + // Restore the update func + this->dataPtr->updateFunc = updateFuncCopy; return *this; } @@ -272,6 +310,32 @@ std::string Param::GetDefaultAsString() const return ss.str(); } +////////////////////////////////////////////////// +std::optional Param::GetMinValueAsString() const +{ + if (this->dataPtr->minValue.has_value()) + { + StringStreamClassicLocale ss; + + ss << ParamStreamer{ *this->dataPtr->minValue }; + return ss.str(); + } + return std::nullopt; +} + +////////////////////////////////////////////////// +std::optional Param::GetMaxValueAsString() const +{ + if (this->dataPtr->maxValue.has_value()) + { + StringStreamClassicLocale ss; + + ss << ParamStreamer{ *this->dataPtr->maxValue }; + return ss.str(); + } + return std::nullopt; +} + ////////////////////////////////////////////////// /// \brief Helper function for Param::ValueFromString /// \param[in] _input Input string. @@ -478,11 +542,19 @@ bool Param::SetFromString(const std::string &_value) return true; } + auto oldValue = this->dataPtr->value; if (!this->ValueFromString(str)) { return false; } + // Check if the value is permitted + if (!this->ValidateValue()) + { + this->dataPtr->value = oldValue; + return false; + } + this->dataPtr->set = true; return this->dataPtr->set; } @@ -497,11 +569,7 @@ void Param::Reset() ////////////////////////////////////////////////// ParamPtr Param::Clone() const { - ParamPtr clone(new Param(this->dataPtr->key, this->dataPtr->typeName, - this->GetAsString(), this->dataPtr->required, - this->dataPtr->description)); - clone->dataPtr->set = this->dataPtr->set; - return clone; + return std::make_shared(*this); } ////////////////////////////////////////////////// @@ -539,3 +607,39 @@ bool Param::GetSet() const { return this->dataPtr->set; } + +bool Param::ValidateValue() const +{ + return std::visit( + [this](const auto &_val) -> bool + { + using T = std::decay_t; + // cppcheck-suppress syntaxError + if constexpr (std::is_scalar_v) + { + if (this->dataPtr->minValue.has_value()) + { + if (_val < std::get(*this->dataPtr->minValue)) + { + sdferr << "The value [" << _val + << "] is less than the minimum allowed value of [" + << *this->GetMinValueAsString() << "] for key [" + << this->GetKey() << "]\n"; + return false; + } + } + if (this->dataPtr->maxValue.has_value()) + { + if (_val > std::get(*this->dataPtr->maxValue)) + { + sdferr << "The value [" << _val + << "] is greater than the maximum allowed value of [" + << *this->GetMaxValueAsString() << "] for key [" + << this->GetKey() << "]\n"; + return false; + } + } + } + return true; + }, this->dataPtr->value); +} diff --git a/src/Param_TEST.cc b/src/Param_TEST.cc index a9c2172c0..686b38ee9 100644 --- a/src/Param_TEST.cc +++ b/src/Param_TEST.cc @@ -391,6 +391,26 @@ TEST(Param, SetTemplate) EXPECT_DOUBLE_EQ(value, 25.456); } +//////////////////////////////////////////////////// +TEST(Param, MinMaxViolation) +{ + sdf::Param doubleParam("key", "double", "1.0", false, "0", "10.0", + "description"); + { + double value; + EXPECT_TRUE(doubleParam.Get(value)); + EXPECT_DOUBLE_EQ(value, 1.0); + } + + EXPECT_FALSE(doubleParam.Set(-1)); + + { + double value; + EXPECT_TRUE(doubleParam.Get(value)); + EXPECT_DOUBLE_EQ(value, 1.0); + } +} + ///////////////////////////////////////////////// /// Main int main(int argc, char **argv) diff --git a/src/parser.cc b/src/parser.cc index 0e42ddb48..309aa831a 100644 --- a/src/parser.cc +++ b/src/parser.cc @@ -224,7 +224,22 @@ bool initXml(TiXmlElement *_xml, ElementPtr _sdf) description = descChild->GetText(); } - _sdf->AddValue(elemTypeString, elemDefaultValue, required, description); + std::string minValue; + const char *elemMinValue = _xml->Attribute("min"); + if (nullptr != elemMinValue) + { + minValue = elemMinValue; + } + + std::string maxValue; + const char *elemMaxValue = _xml->Attribute("max"); + if (nullptr != elemMaxValue) + { + maxValue = elemMaxValue; + } + + _sdf->AddValue(elemTypeString, elemDefaultValue, required, minValue, + maxValue, description); } // Get all attributes diff --git a/src/parser_TEST.cc b/src/parser_TEST.cc index c6348305d..cd562a8d6 100644 --- a/src/parser_TEST.cc +++ b/src/parser_TEST.cc @@ -533,6 +533,100 @@ TEST(Parser, SyntaxErrorInValues) } ///////////////////////////////////////////////// +/// Fixture for setting up stream redirection +class ValueConstraintsFixture : public ::testing::Test +{ + public: void ClearErrorBuffer() + { + this->errBuffer.str(""); + } + + protected: void SetUp() override + { + oldRdbuf = std::cerr.rdbuf(errBuffer.rdbuf()); + } + + protected: void TearDown() override + { + std::cerr.rdbuf(oldRdbuf); + } + + public: std::stringstream errBuffer; + private: std::streambuf *oldRdbuf; +}; + +///////////////////////////////////////////////// +/// Check if minimum/maximum values are valided +TEST_F(ValueConstraintsFixture, ElementMinMaxValues) +{ + std::string sdfDescPath = std::string(PROJECT_SOURCE_PATH) + + "/test/sdf/stricter_semantics_desc.sdf"; + + auto sdfTest = std::make_shared(); + sdf::initFile(sdfDescPath, sdfTest); + + // Initialize the root.sdf description and add our test description as one of + // its children + auto sdf = InitSDF(); + sdf->Root()->AddElementDescription(sdfTest->Root()); + + auto wrapInSdf = [](std::string _xml) -> std::string + { + std::stringstream ss; + ss << "" << _xml + << ""; + return ss.str(); + }; + + { + auto elem = sdf->Root()->Clone(); + sdf::Errors errors; + EXPECT_TRUE(sdf::readString( + wrapInSdf("00"), elem, errors)); + EXPECT_TRUE(errors.empty()) << errors[0]; + } + + auto errorContains = + [](const std::string &_expStr, const std::string &_errs) + { + return _errs.find(_expStr) != std::string::npos; + }; + + { + this->ClearErrorBuffer(); + auto elem = sdf->Root()->Clone(); + EXPECT_FALSE(sdf::readString( + wrapInSdf("-1"), elem)); + EXPECT_PRED2(errorContains, + "The value [-1] is less than the minimum allowed value of [0] " + "for key [int_t]", + this->errBuffer.str()); + } + + { + this->ClearErrorBuffer(); + auto elem = sdf->Root()->Clone(); + EXPECT_FALSE(sdf::readString( + wrapInSdf("-1.0"), elem)); + + EXPECT_PRED2( + errorContains, + "The value [-1] is less than the minimum allowed value of [0] for key " + "[double_t]", + this->errBuffer.str()); + } + + { + this->ClearErrorBuffer(); + auto elem = sdf->Root()->Clone(); + EXPECT_FALSE(sdf::readString(wrapInSdf("20"), elem)); + EXPECT_PRED2( + errorContains, + "The value [20] is greater than the maximum allowed value of [10]", + this->errBuffer.str()); + } +} + ///////////////////////////////////////////////// /// Main int main(int argc, char **argv) diff --git a/src/parser_private.hh b/src/parser_private.hh index 343ef4cbe..40dfd7393 100644 --- a/src/parser_private.hh +++ b/src/parser_private.hh @@ -41,13 +41,29 @@ namespace sdf static std::string getBestSupportedModelVersion(TiXmlElement *_modelXML, std::string &_modelFileName); - /// \brief Initialize the SDF interface using a TinyXML document + /// \brief Initialize the SDF interface using a TinyXML document. + /// + /// This actually forwards to initXml after converting the inputs + /// \param[in] _xmlDoc TinyXML document containing the SDFormat description + /// file that corresponds with the input SDFPtr + /// \param[in] _sdf SDF interface to be initialized static bool initDoc(TiXmlDocument *_xmlDoc, SDFPtr _sdf); - /// \brief Initialize and SDF Element using a TinyXML document + /// \brief Initialize the SDF Element using a TinyXML document + /// + /// This actually forwards to initXml after converting the inputs + /// \param[in] _xmlDoc TinyXML document containing the SDFormat description + /// file that corresponds with the input ElementPtr + /// \param[in] _sdf SDF Element to be initialized static bool initDoc(TiXmlDocument *_xmlDoc, ElementPtr _sdf); - /// \brief For internal use only. Do not use this function. + /// \brief Initialize the SDF Element by parsing the SDFormat description in + /// the input TinyXML element. This is where SDFormat spec/description files + /// are parsed + /// \remark For internal use only. Do not use this function. + /// \param[in] _xml TinyXML element containing the SDFormat description + /// file that corresponds with the input ElementPtr + /// \param[in] _sdf SDF ElementPtr to be initialized static bool initXml(TiXmlElement *_xml, ElementPtr _sdf); /// \brief Populate the SDF values from a TinyXML document @@ -55,11 +71,15 @@ namespace sdf const std::string &_source, bool _convert, Errors &_errors); + /// \brief Populate the SDF values from a TinyXML document static bool readDoc(TiXmlDocument *_xmlDoc, ElementPtr _sdf, const std::string &_source, bool _convert, Errors &_errors); - /// \brief For internal use only. Do not use this function. - /// \param[in] _xml Pointer to the XML document + /// \brief Populate an SDF Element from the XML input. The XML input here is + /// an actual SDFormat file or string, not the description of the SDFormat + /// spec. + /// \remark For internal use only. Do not use this function. + /// \param[in] _xml Pointer to the TinyXML element /// \param[in,out] _sdf SDF pointer to parse data into. /// \param[out] _errors Captures errors found during parsing. /// \return True on success, false on error. diff --git a/test/sdf/stricter_semantics_desc.sdf b/test/sdf/stricter_semantics_desc.sdf new file mode 100644 index 000000000..2638286e3 --- /dev/null +++ b/test/sdf/stricter_semantics_desc.sdf @@ -0,0 +1,5 @@ + + + + + From f2194d2504017856692b452c3f7c47f83d9a9927 Mon Sep 17 00:00:00 2001 From: "Addisu Z. Taddese" Date: Wed, 10 Jun 2020 15:50:02 -0500 Subject: [PATCH 2/5] Style Signed-off-by: Addisu Z. Taddese --- src/parser_TEST.cc | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/parser_TEST.cc b/src/parser_TEST.cc index cd562a8d6..e2da436d4 100644 --- a/src/parser_TEST.cc +++ b/src/parser_TEST.cc @@ -536,16 +536,20 @@ TEST(Parser, SyntaxErrorInValues) /// Fixture for setting up stream redirection class ValueConstraintsFixture : public ::testing::Test { + public: ValueConstraintsFixture() = default; + public: void ClearErrorBuffer() { this->errBuffer.str(""); } + // cppcheck-suppress unusedFunction protected: void SetUp() override { oldRdbuf = std::cerr.rdbuf(errBuffer.rdbuf()); } + // cppcheck-suppress unusedFunction protected: void TearDown() override { std::cerr.rdbuf(oldRdbuf); From 5e718fd8e3a05255cea64977a55dc8abb478888b Mon Sep 17 00:00:00 2001 From: "Addisu Z. Taddese" Date: Wed, 10 Jun 2020 16:06:19 -0500 Subject: [PATCH 3/5] Fix windows test failure Signed-off-by: Addisu Z. Taddese --- src/parser_TEST.cc | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/parser_TEST.cc b/src/parser_TEST.cc index e2da436d4..c4215cf33 100644 --- a/src/parser_TEST.cc +++ b/src/parser_TEST.cc @@ -546,6 +546,7 @@ class ValueConstraintsFixture : public ::testing::Test // cppcheck-suppress unusedFunction protected: void SetUp() override { + sdf::Console::Instance()->SetQuiet(false); oldRdbuf = std::cerr.rdbuf(errBuffer.rdbuf()); } @@ -553,6 +554,9 @@ class ValueConstraintsFixture : public ::testing::Test protected: void TearDown() override { std::cerr.rdbuf(oldRdbuf); +#ifdef _WIN32 + sdf::Console::Instance()->SetQuiet(true); +#endif } public: std::stringstream errBuffer; From 6745d9b1a14df825cb6945d2114d87bfd3a4ae2f Mon Sep 17 00:00:00 2001 From: Steve Peters Date: Wed, 24 Jun 2020 01:11:08 -0700 Subject: [PATCH 4/5] Add /'s and a few test expectations Signed-off-by: Steve Peters --- src/Param.cc | 1 + src/Param_TEST.cc | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Param.cc b/src/Param.cc index 10357cec0..ced4922dc 100644 --- a/src/Param.cc +++ b/src/Param.cc @@ -608,6 +608,7 @@ bool Param::GetSet() const return this->dataPtr->set; } +///////////////////////////////////////////////// bool Param::ValidateValue() const { return std::visit( diff --git a/src/Param_TEST.cc b/src/Param_TEST.cc index 686b38ee9..102490486 100644 --- a/src/Param_TEST.cc +++ b/src/Param_TEST.cc @@ -402,12 +402,14 @@ TEST(Param, MinMaxViolation) EXPECT_DOUBLE_EQ(value, 1.0); } - EXPECT_FALSE(doubleParam.Set(-1)); + EXPECT_FALSE(doubleParam.Set(-1.)); + EXPECT_FALSE(doubleParam.Set(11.)); + EXPECT_TRUE(doubleParam.Set(5.)); { double value; EXPECT_TRUE(doubleParam.Get(value)); - EXPECT_DOUBLE_EQ(value, 1.0); + EXPECT_DOUBLE_EQ(value, 5.0); } } From a450b3c0a5819ac93d2a76cd0a871cb29c997802 Mon Sep 17 00:00:00 2001 From: "Addisu Z. Taddese" Date: Thu, 25 Jun 2020 10:35:25 -0500 Subject: [PATCH 5/5] Update Changelog and Migration Signed-off-by: Addisu Z. Taddese --- Changelog.md | 3 +++ Migration.md | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/Changelog.md b/Changelog.md index 1825451a3..d0fbcd35f 100644 --- a/Changelog.md +++ b/Changelog.md @@ -4,6 +4,9 @@ ### libsdformat 10.0.0 (202X-XX-XX) +1. Enforce minimum/maximum values specified in SDFormat description files + * [Pull request 303](https://github.com/osrf/sdformat/pull/303) + 1. Make parsing of values syntactically more strict with bad values generating an error * [Pull request 244](https://github.com/osrf/sdformat/pull/244) diff --git a/Migration.md b/Migration.md index 556f4360e..e2f8fd9fb 100644 --- a/Migration.md +++ b/Migration.md @@ -12,6 +12,24 @@ forward programmatically. This document aims to contain similar information to those files but with improved human-readability.. +## SDFormat 9.x to 10.0 + +### Modifications + +1. + Minimum/maximum values specified in SDFormat description files are now enforced + + [Pull request 303](https://github.com/osrf/sdformat/pull/303) + +### Additions + +1. **sdf/Element.hh** + + void AddValue(const std::string &, const std::string &, bool, const std::string &, const std::string &, const std::string &) + +1. **sdf/Param.hh** + + Param(const std::string &, const std::string &e, const std::string &, bool, const std::string &, const std::string &, const std::string &) + + std::optional GetMinValueAsString() const; + + std::optional GetMaxValueAsString() const; + + bool ValidateValue() const; + ## SDFormat 8.x to 9.0 ### Additions