mirror of
https://github.com/OPM/ResInsight.git
synced 2026-10-26 07:00:17 -05:00
Add validation to field and object
Add support for optional range checking in PdmDataValueField
This commit is contained in:
1 parent
9fa002abec
commit
f342ca7757
6 files changed
+496
No files matched your search
+373
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user