diff --git a/Fwk/AppFwk/cafProjectDataModel/cafPdmCore/cafPdmCore_UnitTests/cafPdmCoreBasicTest.cpp b/Fwk/AppFwk/cafProjectDataModel/cafPdmCore/cafPdmCore_UnitTests/cafPdmCoreBasicTest.cpp index 88dc3cd3ca..6d6bb998bf 100644 --- a/Fwk/AppFwk/cafProjectDataModel/cafPdmCore/cafPdmCore_UnitTests/cafPdmCoreBasicTest.cpp +++ b/Fwk/AppFwk/cafProjectDataModel/cafPdmCore/cafPdmCore_UnitTests/cafPdmCoreBasicTest.cpp @@ -635,3 +635,376 @@ TEST( BaseTest, testTypedChildren ) delete ihd1; } + +//-------------------------------------------------------------------------------------------------- +/// Test of field validation +//-------------------------------------------------------------------------------------------------- +TEST( BaseTest, FieldValidation ) +{ + // Custom field class with validation + class PositiveDoubleField : public caf::PdmDataValueField + { + public: + QString validate() const override + { + if ( m_fieldValue < 0.0 ) + { + return "Value must be positive"; + } + return QString(); + } + }; + + class TestObject : public caf::PdmObjectHandle + { + public: + TestObject() + { + this->addField( &m_validatedField, "validatedField" ); + this->addField( &m_normalField, "normalField" ); + } + + PositiveDoubleField m_validatedField; + caf::PdmDataValueField m_normalField; + }; + + TestObject* obj = new TestObject; + + // Test valid value + obj->m_validatedField.setValue( 10.0 ); + EXPECT_TRUE( obj->m_validatedField.isValid() ); + EXPECT_TRUE( obj->m_validatedField.validate().isEmpty() ); + + // Test invalid value + obj->m_validatedField.setValue( -5.0 ); + EXPECT_FALSE( obj->m_validatedField.isValid() ); + EXPECT_EQ( "Value must be positive", obj->m_validatedField.validate() ); + + // Test normal field (always valid by default) + obj->m_normalField.setValue( -100.0 ); + EXPECT_TRUE( obj->m_normalField.isValid() ); + EXPECT_TRUE( obj->m_normalField.validate().isEmpty() ); + + delete obj; +} + +//-------------------------------------------------------------------------------------------------- +/// Test of field range validation +//-------------------------------------------------------------------------------------------------- +TEST( BaseTest, FieldRangeValidation ) +{ + class TestObject : public caf::PdmObjectHandle + { + public: + TestObject() + { + this->addField( &m_percentage, "percentage" ); + this->addField( &m_age, "age" ); + this->addField( &m_temperature, "temperature" ); + this->addField( &m_name, "name" ); + } + + caf::PdmDataValueField m_percentage; + caf::PdmDataValueField m_age; + caf::PdmDataValueField m_temperature; + caf::PdmDataValueField m_name; + }; + + TestObject* obj = new TestObject; + + // Test double field with range + obj->m_percentage.setRange( 0.0, 100.0 ); + + obj->m_percentage.setValue( 50.0 ); + EXPECT_TRUE( obj->m_percentage.isValid() ); + EXPECT_TRUE( obj->m_percentage.validate().isEmpty() ); + + obj->m_percentage.setValue( 0.0 ); + EXPECT_TRUE( obj->m_percentage.isValid() ); + + obj->m_percentage.setValue( 100.0 ); + EXPECT_TRUE( obj->m_percentage.isValid() ); + + obj->m_percentage.setValue( -10.0 ); + EXPECT_FALSE( obj->m_percentage.isValid() ); + EXPECT_FALSE( obj->m_percentage.validate().isEmpty() ); + EXPECT_TRUE( obj->m_percentage.validate().contains( "below minimum" ) ); + + obj->m_percentage.setValue( 150.0 ); + EXPECT_FALSE( obj->m_percentage.isValid() ); + EXPECT_TRUE( obj->m_percentage.validate().contains( "exceeds maximum" ) ); + + // Test int field with range + obj->m_age.setRange( 0, 150 ); + + obj->m_age.setValue( 25 ); + EXPECT_TRUE( obj->m_age.isValid() ); + + obj->m_age.setValue( -5 ); + EXPECT_FALSE( obj->m_age.isValid() ); + + obj->m_age.setValue( 200 ); + EXPECT_FALSE( obj->m_age.isValid() ); + + // Test clearing range + obj->m_age.clearRange(); + obj->m_age.setValue( -100 ); + EXPECT_TRUE( obj->m_age.isValid() ); + + obj->m_age.setValue( 1000 ); + EXPECT_TRUE( obj->m_age.isValid() ); + + // Test float field with range + obj->m_temperature.setRange( -273.15f, 1000.0f ); + + obj->m_temperature.setValue( 20.0f ); + EXPECT_TRUE( obj->m_temperature.isValid() ); + + obj->m_temperature.setValue( -300.0f ); + EXPECT_FALSE( obj->m_temperature.isValid() ); + + obj->m_temperature.setValue( 1500.0f ); + EXPECT_FALSE( obj->m_temperature.isValid() ); + + // QString field should not have setRange method (SFINAE should exclude it) + // Uncommenting the following line should cause a compilation error: + // obj->m_name.setRange( "A", "Z" ); + + // QString field should always be valid (no range checking) + obj->m_name.setValue( "Any string value" ); + EXPECT_TRUE( obj->m_name.isValid() ); + + delete obj; +} + +//-------------------------------------------------------------------------------------------------- +/// Test of independent min/max value validation +//-------------------------------------------------------------------------------------------------- +TEST( BaseTest, IndependentMinMaxValidation ) +{ + class TestObject : public caf::PdmObjectHandle + { + public: + TestObject() + { + this->addField( &m_minOnly, "minOnly" ); + this->addField( &m_maxOnly, "maxOnly" ); + this->addField( &m_both, "both" ); + } + + caf::PdmDataValueField m_minOnly; + caf::PdmDataValueField m_maxOnly; + caf::PdmDataValueField m_both; + }; + + TestObject* obj = new TestObject; + + // Test minimum value only + obj->m_minOnly.setMinValue( 0.0 ); + + obj->m_minOnly.setValue( 10.0 ); + EXPECT_TRUE( obj->m_minOnly.isValid() ); + + obj->m_minOnly.setValue( 0.0 ); + EXPECT_TRUE( obj->m_minOnly.isValid() ); + + obj->m_minOnly.setValue( -5.0 ); + EXPECT_FALSE( obj->m_minOnly.isValid() ); + EXPECT_TRUE( obj->m_minOnly.validate().contains( "below minimum" ) ); + + obj->m_minOnly.setValue( 1000000.0 ); // No maximum set, so any large value is valid + EXPECT_TRUE( obj->m_minOnly.isValid() ); + + // Test maximum value only + obj->m_maxOnly.setMaxValue( 100.0 ); + + obj->m_maxOnly.setValue( 50.0 ); + EXPECT_TRUE( obj->m_maxOnly.isValid() ); + + obj->m_maxOnly.setValue( 100.0 ); + EXPECT_TRUE( obj->m_maxOnly.isValid() ); + + obj->m_maxOnly.setValue( 150.0 ); + EXPECT_FALSE( obj->m_maxOnly.isValid() ); + EXPECT_TRUE( obj->m_maxOnly.validate().contains( "exceeds maximum" ) ); + + obj->m_maxOnly.setValue( -1000000.0 ); // No minimum set, so any small value is valid + EXPECT_TRUE( obj->m_maxOnly.isValid() ); + + // Test setting min and max independently + obj->m_both.setMinValue( 0 ); + obj->m_both.setValue( -1 ); + EXPECT_FALSE( obj->m_both.isValid() ); + + obj->m_both.setValue( 100 ); + EXPECT_TRUE( obj->m_both.isValid() ); // No max set yet + + obj->m_both.setMaxValue( 50 ); + EXPECT_FALSE( obj->m_both.isValid() ); // Now exceeds max + + obj->m_both.setValue( 25 ); + EXPECT_TRUE( obj->m_both.isValid() ); + + // Test clearing individual limits + obj->m_both.clearMinValue(); + obj->m_both.setValue( -100 ); + EXPECT_TRUE( obj->m_both.isValid() ); // Min cleared, but max still applies + + obj->m_both.setValue( 100 ); + EXPECT_FALSE( obj->m_both.isValid() ); // Max still active + + obj->m_both.clearMaxValue(); + EXPECT_TRUE( obj->m_both.isValid() ); // Both limits cleared + + delete obj; +} + +//-------------------------------------------------------------------------------------------------- +/// Test of object-level validation +//-------------------------------------------------------------------------------------------------- +TEST( BaseTest, ObjectValidation ) +{ + class ValidatedObject : public caf::PdmObjectHandle + { + public: + ValidatedObject() + { + this->addField( &m_age, "age" ); + this->addField( &m_percentage, "percentage" ); + this->addField( &m_name, "name" ); + + // Set up field validation ranges + m_age.setRange( 0, 150 ); + m_percentage.setRange( 0.0, 100.0 ); + } + + caf::PdmDataValueField m_age; + caf::PdmDataValueField m_percentage; + caf::PdmDataValueField m_name; + }; + + // Custom object with overridden validation using configName + class CustomValidatedObject : public ValidatedObject + { + public: + std::map validate( const QString& configName = "" ) const override + { + // First validate all fields using base implementation + auto fieldErrors = ValidatedObject::validate( configName ); + + // Add custom object-level validation + if ( m_name.value().isEmpty() ) + { + fieldErrors["name"] = "Name cannot be empty"; + } + + // Config-specific validation + if ( configName == "strict" ) + { + if ( m_age.value() < 18 && m_percentage.value() > 50.0 ) + { + fieldErrors["_object"] = "Percentage must be <= 50 for age < 18 (strict mode)"; + } + } + else if ( configName == "relaxed" ) + { + // No additional validation in relaxed mode + } + else + { + // Default validation + if ( m_age.value() < 18 && m_percentage.value() > 50.0 ) + { + fieldErrors["_object"] = "Percentage must be <= 50 for age < 18"; + } + } + + return fieldErrors; + } + }; + + // Test basic object validation + ValidatedObject* obj = new ValidatedObject; + + obj->m_age.setValue( 25 ); + obj->m_percentage.setValue( 75.0 ); + obj->m_name.setValue( "Test" ); + + EXPECT_TRUE( obj->isValid() ); + EXPECT_TRUE( obj->validate().empty() ); + + // Test with invalid field + obj->m_age.setValue( -5 ); + EXPECT_FALSE( obj->isValid() ); + + auto errors = obj->validate(); + EXPECT_EQ( 1u, errors.size() ); + EXPECT_TRUE( errors.find( "age" ) != errors.end() ); + EXPECT_TRUE( errors["age"].contains( "below minimum" ) ); + + // Test with multiple invalid fields + obj->m_percentage.setValue( 150.0 ); + errors = obj->validate(); + EXPECT_EQ( 2u, errors.size() ); + EXPECT_TRUE( errors.find( "age" ) != errors.end() ); + EXPECT_TRUE( errors.find( "percentage" ) != errors.end() ); + + // Fix all fields + obj->m_age.setValue( 30 ); + obj->m_percentage.setValue( 50.0 ); + EXPECT_TRUE( obj->isValid() ); + + delete obj; + + // Test custom validation + CustomValidatedObject* customObj = new CustomValidatedObject; + + customObj->m_age.setValue( 25 ); + customObj->m_percentage.setValue( 75.0 ); + customObj->m_name.setValue( "John" ); + + EXPECT_TRUE( customObj->isValid() ); + + // Test custom validation: empty name + customObj->m_name.setValue( "" ); + EXPECT_FALSE( customObj->isValid() ); + + errors = customObj->validate(); + EXPECT_TRUE( errors.find( "name" ) != errors.end() ); + EXPECT_EQ( "Name cannot be empty", errors["name"] ); + + // Test custom validation: age < 18 with percentage > 50 + customObj->m_name.setValue( "Jane" ); + customObj->m_age.setValue( 15 ); + customObj->m_percentage.setValue( 60.0 ); + + EXPECT_FALSE( customObj->isValid() ); + errors = customObj->validate(); + EXPECT_TRUE( errors.find( "_object" ) != errors.end() ); + EXPECT_EQ( "Percentage must be <= 50 for age < 18", errors["_object"] ); + + // Fix the percentage + customObj->m_percentage.setValue( 40.0 ); + EXPECT_TRUE( customObj->isValid() ); + + // Test config-specific validation: strict mode + customObj->m_age.setValue( 15 ); + customObj->m_percentage.setValue( 60.0 ); + + errors = customObj->validate( "strict" ); + EXPECT_FALSE( errors.empty() ); + EXPECT_TRUE( errors.find( "_object" ) != errors.end() ); + EXPECT_TRUE( errors["_object"].contains( "strict mode" ) ); + + // Test config-specific validation: relaxed mode (no age/percentage validation) + errors = customObj->validate( "relaxed" ); + EXPECT_TRUE( errors.empty() ); // No age/percentage rule in relaxed mode + + // Test default mode (same as no config name) + errors = customObj->validate(); + EXPECT_FALSE( errors.empty() ); + EXPECT_TRUE( errors.find( "_object" ) != errors.end() ); + EXPECT_FALSE( errors["_object"].contains( "strict mode" ) ); + + delete customObj; +} diff --git a/Fwk/AppFwk/cafProjectDataModel/cafPdmCore/cafPdmDataValueField.h b/Fwk/AppFwk/cafProjectDataModel/cafPdmCore/cafPdmDataValueField.h index ab422dae4a..9747b3a534 100644 --- a/Fwk/AppFwk/cafProjectDataModel/cafPdmCore/cafPdmDataValueField.h +++ b/Fwk/AppFwk/cafProjectDataModel/cafPdmCore/cafPdmDataValueField.h @@ -45,6 +45,8 @@ #include +#include +#include #include #include @@ -131,6 +133,48 @@ public: bool operator==( const DataType& fieldValue ) const { return m_fieldValue == fieldValue; } bool operator!=( const DataType& fieldValue ) const { return m_fieldValue != fieldValue; } + // Range validation - only available for arithmetic types + template + typename std::enable_if::value>::type setRange( const DataType& minValue, const DataType& maxValue ) + { + m_minValue = std::min( minValue, maxValue ); + m_maxValue = std::max( minValue, maxValue ); + } + + template + typename std::enable_if::value>::type clearRange() + { + m_minValue.reset(); + m_maxValue.reset(); + } + + template + typename std::enable_if::value>::type setMinValue( const DataType& minValue ) + { + m_minValue = minValue; + } + + template + typename std::enable_if::value>::type setMaxValue( const DataType& maxValue ) + { + m_maxValue = maxValue; + } + + template + typename std::enable_if::value>::type clearMinValue() + { + m_minValue.reset(); + } + + template + typename std::enable_if::value>::type clearMaxValue() + { + m_maxValue.reset(); + } + + // Override validate from PdmFieldHandle + QString validate() const override; + protected: DataType m_fieldValue; @@ -144,6 +188,10 @@ public: protected: DataType m_defaultFieldValue; + + // Range validation members + std::optional m_minValue; + std::optional m_maxValue; }; //-------------------------------------------------------------------------------------------------- @@ -171,4 +219,25 @@ void caf::PdmDataValueField::setValueWithFieldChanged( const DataType& } } +//-------------------------------------------------------------------------------------------------- +/// Validates the field value against optional range constraints. +/// Empty QString means valid, non-empty QString contains error message. +//-------------------------------------------------------------------------------------------------- +template +QString caf::PdmDataValueField::validate() const +{ + if constexpr ( std::is_arithmetic::value ) + { + if ( m_minValue.has_value() && m_fieldValue < m_minValue.value() ) + { + return QString( "Value %1 is below minimum %2" ).arg( m_fieldValue ).arg( m_minValue.value() ); + } + if ( m_maxValue.has_value() && m_fieldValue > m_maxValue.value() ) + { + return QString( "Value %1 exceeds maximum %2" ).arg( m_fieldValue ).arg( m_maxValue.value() ); + } + } + return QString(); +} + } // End of namespace caf diff --git a/Fwk/AppFwk/cafProjectDataModel/cafPdmCore/cafPdmFieldHandle.cpp b/Fwk/AppFwk/cafProjectDataModel/cafPdmCore/cafPdmFieldHandle.cpp index e2178856fb..94cfd31403 100644 --- a/Fwk/AppFwk/cafProjectDataModel/cafPdmCore/cafPdmFieldHandle.cpp +++ b/Fwk/AppFwk/cafProjectDataModel/cafPdmCore/cafPdmFieldHandle.cpp @@ -129,6 +129,23 @@ std::vector PdmFieldHandle::keywordAliases() const return m_keywordAliases; } +//-------------------------------------------------------------------------------------------------- +/// Default validation returns empty string (valid). Override in derived classes to implement +/// custom validation logic. Empty QString means valid, non-empty QString contains error message. +//-------------------------------------------------------------------------------------------------- +QString PdmFieldHandle::validate() const +{ + return QString(); +} + +//-------------------------------------------------------------------------------------------------- +/// Convenience method to check if field is valid. +//-------------------------------------------------------------------------------------------------- +bool PdmFieldHandle::isValid() const +{ + return validate().isEmpty(); +} + // These two functions can be used when PdmCore is used standalone without PdmUi/PdmXml /* PdmUiFieldHandle* PdmFieldHandle::uiCapability() diff --git a/Fwk/AppFwk/cafProjectDataModel/cafPdmCore/cafPdmFieldHandle.h b/Fwk/AppFwk/cafProjectDataModel/cafPdmCore/cafPdmFieldHandle.h index 9ee7eaa016..1601bafa56 100644 --- a/Fwk/AppFwk/cafProjectDataModel/cafPdmCore/cafPdmFieldHandle.h +++ b/Fwk/AppFwk/cafProjectDataModel/cafPdmCore/cafPdmFieldHandle.h @@ -58,6 +58,10 @@ public: PdmUiFieldHandle* uiCapability(); PdmXmlFieldHandle* xmlCapability(); + // Validation + virtual QString validate() const; + bool isValid() const; + protected: bool isInitializedByInitFieldMacro() const { return m_ownerObject != nullptr; } diff --git a/Fwk/AppFwk/cafProjectDataModel/cafPdmCore/cafPdmObjectHandle.cpp b/Fwk/AppFwk/cafProjectDataModel/cafPdmCore/cafPdmObjectHandle.cpp index 82e0380167..ead7280b63 100644 --- a/Fwk/AppFwk/cafProjectDataModel/cafPdmCore/cafPdmObjectHandle.cpp +++ b/Fwk/AppFwk/cafProjectDataModel/cafPdmCore/cafPdmObjectHandle.cpp @@ -259,6 +259,34 @@ void PdmObjectHandle::onChildDeleted( PdmChildArrayFieldHandle* childA { } +//-------------------------------------------------------------------------------------------------- +/// Default validation validates all fields in the object. +/// Derived classes can override to add custom object-level validation. +//-------------------------------------------------------------------------------------------------- +std::map PdmObjectHandle::validate( const QString& configName ) const +{ + std::map fieldErrors; + + for ( PdmFieldHandle* field : m_fields ) + { + QString error = field->validate(); + if ( !error.isEmpty() ) + { + fieldErrors[field->keyword()] = error; + } + } + + return fieldErrors; +} + +//-------------------------------------------------------------------------------------------------- +/// Convenience method to check if object is valid. +//-------------------------------------------------------------------------------------------------- +bool PdmObjectHandle::isValid( const QString& configName ) const +{ + return validate( configName ).empty(); +} + // These two functions can be used when PdmCore is used standalone without PdmUi/PdmXml /* PdmUiObjectHandle* PdmObjectHandle::uiCapability() diff --git a/Fwk/AppFwk/cafProjectDataModel/cafPdmCore/cafPdmObjectHandle.h b/Fwk/AppFwk/cafProjectDataModel/cafPdmCore/cafPdmObjectHandle.h index e256870397..dc845e2912 100644 --- a/Fwk/AppFwk/cafProjectDataModel/cafPdmCore/cafPdmObjectHandle.h +++ b/Fwk/AppFwk/cafProjectDataModel/cafPdmCore/cafPdmObjectHandle.h @@ -6,6 +6,7 @@ #include +#include #include #include @@ -96,6 +97,10 @@ public: PdmUiObjectHandle* uiCapability() const; // Implementation is in cafPdmUiObjectHandle.cpp PdmXmlObjectHandle* xmlCapability() const; // Implementation is in cafPdmXmlObjectHandle.cpp + // Validation + [[nodiscard]] virtual std::map validate( const QString& configName = "" ) const; + [[nodiscard]] bool isValid( const QString& configName = "" ) const; + virtual void setDeletable( bool isDeletable ); virtual bool isDeletable() const; virtual void onChildDeleted( PdmChildArrayFieldHandle* childArray,