Add support for custom validation callbacks on fields

* Add support for custom validation callbacks on fields

Introduce setCustomValidationCallback to PdmFieldHandle, allowing custom validation logic via user-provided lambdas or functions. The validate() method now invokes this callback if set, in addition to built-in checks. Includes comprehensive unit tests for custom and combined validation scenarios. Backward compatible: default behavior unchanged if no callback is set.
This commit is contained in:
Magne Sjaastad authored and GitHub committed 2026-02-09 16:48:42 +01:00
1 parent 93bd0b9c9d
commit cb8a069ccd
6 files changed
+141 -16

No files matched your search

@@ -12,6 +12,7 @@
#include "cafPdmReferenceHelper.h"
#include "cafPdmValueField.h"
#include <memory>
#include <vector>
class DemoPdmObject : public caf::PdmObjectHandle
@@ -1008,3 +1009,87 @@ TEST( BaseTest, ObjectValidation )
delete customObj;
}
//--------------------------------------------------------------------------------------------------
/// Test of custom validation callback
//--------------------------------------------------------------------------------------------------
TEST( BaseTest, CustomValidationCallback )
{
class TestObject : public caf::PdmObjectHandle
{
public:
TestObject()
{
this->addField( &m_value, "value" );
this->addField( &m_rangedValue, "rangedValue" );
}
caf::PdmDataValueField<int> m_value;
caf::PdmDataValueField<int> m_rangedValue;
};
auto obj = std::make_unique<TestObject>();
// A field with no callback is valid by default
obj->m_value.setValue( 42 );
EXPECT_TRUE( obj->m_value.isValid() );
EXPECT_TRUE( obj->m_value.validate().isEmpty() );
// Registering a callback that returns an error string makes the field invalid
obj->m_value.setCustomValidationCallback( []() -> QString { return "Custom error"; } );
EXPECT_FALSE( obj->m_value.isValid() );
EXPECT_EQ( QString( "Custom error" ), obj->m_value.validate() );
// Registering a callback that returns empty string keeps the field valid
obj->m_value.setCustomValidationCallback( []() -> QString { return QString(); } );
EXPECT_TRUE( obj->m_value.isValid() );
EXPECT_TRUE( obj->m_value.validate().isEmpty() );
// The callback can access the field value for conditional validation
obj->m_value.setCustomValidationCallback(
[&obj]() -> QString
{
if ( obj->m_value.value() % 2 != 0 )
{
return "Value must be even";
}
return QString();
} );
obj->m_value.setValue( 4 );
EXPECT_TRUE( obj->m_value.isValid() );
obj->m_value.setValue( 7 );
EXPECT_FALSE( obj->m_value.isValid() );
EXPECT_EQ( QString( "Value must be even" ), obj->m_value.validate() );
// Custom callback works alongside range validation (both must pass)
obj->m_rangedValue.setRange( 0, 100 );
obj->m_rangedValue.setCustomValidationCallback(
[&obj]() -> QString
{
if ( obj->m_rangedValue.value() % 2 != 0 )
{
return "Value must be even";
}
return QString();
} );
// Both range and custom pass
obj->m_rangedValue.setValue( 50 );
EXPECT_TRUE( obj->m_rangedValue.isValid() );
// Range fails (custom would pass)
obj->m_rangedValue.setValue( 150 );
EXPECT_FALSE( obj->m_rangedValue.isValid() );
EXPECT_TRUE( obj->m_rangedValue.validate().contains( "exceeds maximum" ) );
// Custom fails (range would pass)
obj->m_rangedValue.setValue( 51 );
EXPECT_FALSE( obj->m_rangedValue.isValid() );
EXPECT_EQ( QString( "Value must be even" ), obj->m_rangedValue.validate() );
// Both pass
obj->m_rangedValue.setValue( 50 );
EXPECT_TRUE( obj->m_rangedValue.isValid() );
}
@@ -259,7 +259,7 @@ QString caf::PdmDataValueField<DataType>::validate() const
return QString( "Value %1 exceeds maximum %2" ).arg( m_fieldValue ).arg( m_maxValue.value() );
}
}
return QString();
return executeCustomValidation();
}
} // End of namespace caf
@@ -135,6 +135,26 @@ std::vector<QString> PdmFieldHandle::keywordAliases() const
//--------------------------------------------------------------------------------------------------
QString PdmFieldHandle::validate() const
{
return executeCustomValidation();
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void PdmFieldHandle::setCustomValidationCallback( std::function<QString()> callback )
{
m_customValidationCallback = std::move( callback );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
QString PdmFieldHandle::executeCustomValidation() const
{
if ( m_customValidationCallback )
{
return m_customValidationCallback();
}
return QString();
}
@@ -2,6 +2,7 @@
#include "cafPdmBase.h"
#include <QString>
#include <functional>
#include <vector>
namespace caf
@@ -61,9 +62,11 @@ public:
// Validation
virtual QString validate() const;
bool isValid() const;
void setCustomValidationCallback( std::function<QString()> callback );
protected:
bool isInitializedByInitFieldMacro() const { return m_ownerObject != nullptr; }
QString executeCustomValidation() const;
bool isInitializedByInitFieldMacro() const { return m_ownerObject != nullptr; }
private:
PDM_DISABLE_COPY_AND_ASSIGN( PdmFieldHandle );
@@ -77,6 +80,8 @@ private:
std::vector<QString> m_keywordAliases;
std::vector<std::pair<PdmFieldCapability*, bool>> m_capabilities;
std::function<QString()> m_customValidationCallback;
};
//--------------------------------------------------------------------------------------------------
@@ -26,18 +26,36 @@ ValidationTestObject::ValidationTestObject()
m_percentage.uiCapability()->setUiToolTip( "Valid range: 0 to 100%" );
m_percentage.setRange( 0.0, 100.0 );
// Count field: minimum value only (>= 0)
// Count field: minimum value only (>= 0) and must be even (custom callback)
CAF_PDM_InitField( &m_count, "count", 0, "Count", "", "", "" );
m_count.uiCapability()->setUiToolTip( "Must be non-negative" );
m_count.uiCapability()->setUiToolTip( "Must be non-negative and even" );
m_count.setMinValue( 0 );
m_count.setCustomValidationCallback(
[this]() -> QString
{
if ( m_count() % 2 != 0 )
{
return "Count must be an even number";
}
return {};
} );
// Name field: no validation
CAF_PDM_InitField( &m_name, "name", QString( "John Doe" ), "Name", "", "", "" );
m_name.uiCapability()->setUiToolTip( "Free text field" );
// Email field: no validation (but could add custom validation in validate())
// Email field: custom validation callback
CAF_PDM_InitField( &m_email, "email", QString( "john@example.com" ), "Email", "", "", "" );
m_email.uiCapability()->setUiToolTip( "Email address" );
m_email.uiCapability()->setUiToolTip( "Email address (must contain @)" );
m_email.setCustomValidationCallback(
[this]() -> QString
{
if ( !m_email().isEmpty() && !m_email().contains( "@" ) )
{
return "Email must contain @ symbol";
}
return {};
} );
// Range field: -100.0 to 100.0
CAF_PDM_InitField( &m_rangeField, "rangeField", 0.0, "Range Field", "", "", "" );
@@ -54,15 +72,7 @@ std::map<QString, QString> ValidationTestObject::validate( const QString& config
auto errors = PdmObject::validate( configName );
// Add custom object-level validation
// Email validation (basic)
if ( !m_email().isEmpty() )
{
if ( !m_email().contains( "@" ) )
{
errors["email"] = "Email must contain @ symbol";
}
}
// Note: email validation is handled by a custom validation callback on the field itself
// Name validation
if ( m_name().isEmpty() )
@@ -135,6 +145,7 @@ void ValidationTestObject::performValidation()
qDebug() << "Age valid:" << m_age.isValid();
qDebug() << "Percentage valid:" << m_percentage.isValid();
qDebug() << "Count valid:" << m_count.isValid();
qDebug() << "Email valid:" << m_email.isValid();
// Show validation messages
if ( !m_temperature.isValid() )
@@ -153,4 +164,8 @@ void ValidationTestObject::performValidation()
{
qDebug() << " Count error:" << m_count.validate();
}
if ( !m_email.isValid() )
{
qDebug() << " Email error:" << m_email.validate();
}
}