Add validation to field and object

Add support for optional range checking in PdmDataValueField
This commit is contained in:
Magne Sjaastad committed 2025-12-16 13:01:31 +01:00
1 parent 9fa002abec
commit f342ca7757
6 files changed
+496

No files matched your search

@@ -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<double>
{
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<double> 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<double> m_percentage;
caf::PdmDataValueField<int> m_age;
caf::PdmDataValueField<float> m_temperature;
caf::PdmDataValueField<QString> 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<double> m_minOnly;
caf::PdmDataValueField<double> m_maxOnly;
caf::PdmDataValueField<int> 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<int> m_age;
caf::PdmDataValueField<double> m_percentage;
caf::PdmDataValueField<QString> m_name;
};
// Custom object with overridden validation using configName
class CustomValidatedObject : public ValidatedObject
{
public:
std::map<QString, QString> 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;
}
@@ -45,6 +45,8 @@
#include <QVariant>
#include <optional>
#include <type_traits>
#include <typeinfo>
#include <vector>
@@ -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 T = DataType>
typename std::enable_if<std::is_arithmetic<T>::value>::type setRange( const DataType& minValue, const DataType& maxValue )
{
m_minValue = std::min( minValue, maxValue );
m_maxValue = std::max( minValue, maxValue );
}
template <typename T = DataType>
typename std::enable_if<std::is_arithmetic<T>::value>::type clearRange()
{
m_minValue.reset();
m_maxValue.reset();
}
template <typename T = DataType>
typename std::enable_if<std::is_arithmetic<T>::value>::type setMinValue( const DataType& minValue )
{
m_minValue = minValue;
}
template <typename T = DataType>
typename std::enable_if<std::is_arithmetic<T>::value>::type setMaxValue( const DataType& maxValue )
{
m_maxValue = maxValue;
}
template <typename T = DataType>
typename std::enable_if<std::is_arithmetic<T>::value>::type clearMinValue()
{
m_minValue.reset();
}
template <typename T = DataType>
typename std::enable_if<std::is_arithmetic<T>::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<DataType> m_minValue;
std::optional<DataType> m_maxValue;
};
//--------------------------------------------------------------------------------------------------
@@ -171,4 +219,25 @@ void caf::PdmDataValueField<DataType>::setValueWithFieldChanged( const DataType&
}
}
//--------------------------------------------------------------------------------------------------
/// Validates the field value against optional range constraints.
/// Empty QString means valid, non-empty QString contains error message.
//--------------------------------------------------------------------------------------------------
template <typename DataType>
QString caf::PdmDataValueField<DataType>::validate() const
{
if constexpr ( std::is_arithmetic<DataType>::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
@@ -129,6 +129,23 @@ std::vector<QString> 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()
@@ -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; }
@@ -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<QString, QString> PdmObjectHandle::validate( const QString& configName ) const
{
std::map<QString, QString> 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()
@@ -6,6 +6,7 @@
#include <QString>
#include <map>
#include <set>
#include <vector>
@@ -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<QString, QString> 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,