Menu

Example

Marat

Message

Take for example the following message:

message Person {
  required string name = 1;
  required int32 id = 2;
  optional string email = 3; 
  enum PhoneType {
    MOBILE = 0;
    HOME = 1;
    WORK = 2;
  }
  message PhoneNumber {
    required string number = 1;
    optional PhoneType type = 2 [default = HOME];
  }  
  repeated PhoneNumber phone = 4;
}
  • Person has two required fields: name and id
  • Optional field: email
  • List of telephone numbers, designed as a repeating message, embedded within the message Person
  • Each telephone number has a string value and type
  • The default number type is HOME
  • Natural number after symbol'=' is the tag field
  • The numbering starts with 1

DTO Classes

Let's design a DTO (Data Transfer Object) classes corresponding to this Message.

TPhoneType = (ptMOBILE, ptHOME, ptWORK);

TPhoneNumber = class
private
  FTyp: TPhoneType;
  FNumber: AnsiString;
const
  ft_Number = 1;
  ft_Typ = 2;
public
  constructor Create;
  property Number: AnsiString read FNumber write FNumber;
  property Typ: TPhoneType read FTyp write FTyp;
end;

TPerson = class
private
  FName: AnsiString;
  FEmail: AnsiString;
  FId: integer;
  FPhones: TObjectList;
  function GetPhones(Index: integer): TPhoneNumber;
  function GetPhonesCount: integer;
const
  ft_Name = 1;
  ft_Id = 2;
  ft_Email = 3;
  ft_Phone = 4;
public
  constructor Create;
  destructor Destroy; override;
  procedure AddPhone(const Number: AnsiString; Typ: TPhoneType = ptHOME);
  procedure DeletePhone(Index: integer);
  property Name: AnsiString read FName write FName;
  property Id: integer read FId write FId;
  property Email: AnsiString read FEmail write FEmail;
  property PhonesCount: integer read GetPhonesCount;
  property Phones[Index: integer]: TPhoneNumber read GetPhones;
end;

Classes for loading / unloading

I like it when the functionality to load and unload excluded from DTO classes. The class should do what that one function and do it well.

Classes unloading facilities can store anywhere. Classes downloads can instantiate DTO classes from anywhere.

Although nothing prevents implement methods for loading / unloading directly into DTO classes. You can implement a DTO class constructor with a parameter containing the name of the file or stream to download and implement a method to record the save.

In the present example, the objects are created for embedded messages. This could be compared to the code generated by the utility protoc on base message schema.

It would be nice to have a tool for this library. I have a dream to make this tool. I do not want the community of fans of Delphi and Pascal were anything infringed.

TPersonBuilder = class
private
  FBuffer: TProtoBufOutput;
public
  constructor Create;
  destructor Destroy; override;
  function GetBuf: TProtoBufOutput;
  procedure Write(Person: TPerson);
end;

TPersonReader = class
private
  FBuffer: TProtoBufInput;
  procedure LoadPhone(Phone: TPhoneNumber);
public
  constructor Create;
  destructor Destroy; override;
  function GetBuf: TProtoBufInput;
  procedure Load(person: TPerson);
end;

TPersonBuilder.Write(Person: TPerson)

To write an object is used class TProtoBufOutput.
He has a method to write a fields of different types.
In the method of entry must specify the number of unloading field.
This field number with wireType is used to create a tag.

tag := (fieldNumber shl 3) or wireType;

What rules should be done?

If the field has a default value, then you have to unload it when its value is different from the default

If the field is not required, then do not unload it when the field is empty

procedure TPersonBuilder.Write(Person: TPerson);
var
  Phone: TPhoneNumber;
  PhonesBuffer: TProtoBufOutput;
  i: Integer;
begin
  FBuffer.writeString(TPerson.ft_Name, Person.Name);
  FBuffer.writeInt32(TPerson.ft_Id, Person.FId);
  // Not save empty e-mail
  if Person.Email <> '' then
    FBuffer.writeString(TPerson.ft_Email, Person.Email);
  // Save Phones as Message
  if Person.GetPhonesCount > 0 then
  begin
    PhonesBuffer := TProtoBufOutput.Create;
    try
      // Write person's phones
      for i := 0 to Person.GetPhonesCount - 1 do
      begin
        PhonesBuffer.Clear;
        Phone := Person.Phones[i];
        PhonesBuffer.writeString(TPhoneNumber.ft_Number, Phone.Number);
        // Not save phone type with Default value = ptHOME
        if Phone.FTyp <> ptHOME then
          PhonesBuffer.writeInt32(TPhoneNumber.ft_Typ, Ord(Phone.FTyp));
        // Write phones as message
        FBuffer.writeMessage(TPerson.ft_Phone, PhonesBuffer);
      end;
    finally
      PhonesBuffer.Free;
    end;
  end;
end;

TPersonReader.Load(person: TPerson)

Let's see the code for loading the class. Parameter person: TPerson must contain an empty class. Otherwise, the value of the fields will change to having a phone add new phones.
Perhaps this is what you want. Perhaps, it would be to implement a method clearing the DTO class and call before load.

Let's, first read the tag. Empty value tag means that we are at the bottom, and we have nothing more to read. Therefore, we will use a loop like while in which we will compare the value of the tag to 0.

Tag contains a packed type and number of fields.
For convenience checks, keep these values ​​to local variables. Then we will use a case statement to the number field.

The case statement is basically a controlled GOTO to the address table and is very fast. That the table has a compact, it is desirable that field numbers were consecutive and not scatter.

procedure TPersonReader.Load(person: TPerson);
var
  tag, fieldNumber, wireType: integer;
  Phone: TPhoneNumber;
begin
  tag := FBuffer.readTag;
  while tag <> 0 do begin
    wireType := getTagWireType(tag);
    fieldNumber := getTagFieldNumber(tag);
    case fieldNumber of
      TPerson.ft_Name:
        begin
          Assert(wireType = WIRETYPE_LENGTH_DELIMITED);
          person.Name := FBuffer.readString;
        end;
      TPerson.ft_Id:
        begin
          Assert(wireType = WIRETYPE_VARINT);
          person.Id := FBuffer.readInt32;
        end;
      TPerson.ft_Email:
        begin
          Assert(wireType = WIRETYPE_LENGTH_DELIMITED);
          person.Email := FBuffer.readString;
        end;
      TPerson.ft_Phone:
        begin
          Assert(wireType = WIRETYPE_LENGTH_DELIMITED);
          Phone := TPhoneNumber.Create;
          Person.FPhones.Add(Phone);
          LoadPhone(Phone);
        end;
      else
        FBuffer.skipField(tag);
    end;
    tag := FBuffer.readTag;
  end;
end;

TPersonReader.LoadPhone(Phone: TPhoneNumber)

The data is saved to the phone as a embedded message. The structure of the message contains: a tag, message length and content fields phone.
Knowing this, it is easy to write the required code. He is not much different than loading Person, except: reading record size, need to check whether we are not out of the record size.

procedure TPersonReader.LoadPhone(Phone: TPhoneNumber);
var
  tag, fieldNumber, wireType: integer;
  size: Integer;
  endPosition: Integer;
begin
  size := FBuffer.readInt32;
  endPosition := FBuffer.getPos + size;
  repeat
    tag := FBuffer.readTag;
    if tag = 0 then exit;
    wireType := getTagWireType(tag);
    fieldNumber := getTagFieldNumber(tag);
    case fieldNumber of
      TPhoneNumber.ft_Number:
        begin
          Assert(wireType = WIRETYPE_LENGTH_DELIMITED);
          Phone.Number := FBuffer.readString;
        end;
      TPhoneNumber.ft_Typ:
        begin
          Assert(wireType = WIRETYPE_VARINT);
          Phone.Typ := TPhoneType(FBuffer.readInt32);
        end;
      else
        FBuffer.skipField(tag);
    end;
  until FBuffer.getPos >= endPosition;
end;

Want the latest updates on software, tech news, and AI?
Get latest updates about software, tech news, and AI from SourceForge directly in your inbox once a month.