Overview

Attribute-Based Validation

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.

Source Code

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');

Result

<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>

On this page