Practical demonstration of using class-level attributes and RTTI for metadata-driven object validation. This example shows how to define constraints at the class level and build a validation engine that enforces them.
type
// Define custom attribute classes
RequiredAttribute = class(TCustomAttribute)
FieldName: String;
constructor Create(aFieldName: String);
begin
FieldName := aFieldName;
end;
end;
type
RangeAttribute = class(TCustomAttribute)
FieldName: String;
Min, Max: Integer;
constructor Create(aFieldName: String; aMin, aMax: Integer);
begin
FieldName := aFieldName; Min := aMin; Max := aMax;
end;
end;
type
// A data record with validation constraints defined at the class level
[RequiredAttribute('UserName')]
[RangeAttribute('Age', 18, 120)]
TUserProfile = class
published
UserName: String;
Age: Integer;
Email: String;
end;
// A generic validator that uses RTTI to check objects based on class attributes
function ValidateObject(obj: TObject; typeInfo: TRTTITypeInfo): array of String;
begin
var errors: array of String;
var attrs := RTTIRawAttributes;
// 1. First, collect all available property getters for this type
// In DWScript, we use RTTIPropertyAttribute to access published members dynamically
var props: array [String] of RTTIPropertyAttribute;
for var i := 0 to attrs.Length - 1 do begin
if (attrs[i].T = typeInfo) and (attrs[i].A is RTTIPropertyAttribute) then begin
var p := RTTIPropertyAttribute(attrs[i].A);
props[p.Name] := p;
end;
end;
// 2. Process validation attributes
for var i := 0 to attrs.Length - 1 do begin
if attrs[i].T = typeInfo then begin
var attr := attrs[i].A;
// Process [Required]
if attr is RequiredAttribute then begin
var req := RequiredAttribute(attr);
var p := props[req.FieldName];
if (p <> nil) and (VarToStr(p.Getter(obj)) = '') then
errors.Add(Format('Field "%s" is required but was empty.', [req.FieldName]));
end;
// Process [Range]
if attr is RangeAttribute then begin
var rng := RangeAttribute(attr);
var p := props[rng.FieldName];
if p <> nil then begin
var val := Integer(p.Getter(obj));
if (val < rng.Min) or (val > rng.Max) then
errors.Add(Format('Field "%s" must be between %d and %d (Current: %d).',
[rng.FieldName, rng.Min, rng.Max, val]));
end;
end;
end;
end;
Result := errors;
end;
procedure TestValidation(user: TUserProfile; label: String);
begin
PrintLn('<b>Testing: ' + label + '</b>');
var errors := ValidateObject(user, TypeOf(TUserProfile));
if errors.Length = 0 then
PrintLn('<span style="color:green">Valid!</span>')
else begin
for var err in errors do
PrintLn('<span style="color:red">- ' + err + '</span>');
end;
PrintLn('');
end;
// Execution
var user1 := new TUserProfile;
user1.UserName := 'Alice';
user1.Age := 25;
TestValidation(user1, 'Valid User');
var user2 := new TUserProfile;
user2.UserName := ''; // Violation: Required
user2.Age := 12; // Violation: Range
TestValidation(user2, 'Invalid User');
<b>Testing: Valid User</b> <span style="color:green">Valid!</span> <b>Testing: Invalid User</b> <span style="color:red">- Field "UserName" is required but was empty.</span> <span style="color:red">- Field "Age" must be between 18 and 120 (Current: 12).</span>