Home > Blockchain >  How to differentiate empty string from no value in delphi
How to differentiate empty string from no value in delphi

Time:01-31

I saw in this question: Empty string becomes null when passed from Delphi to C# as a function argument that Delphi's empty string value in reality is just a null-pointer - which I understand the reasoning behind.

I do have an issue though as I am developing a Web API in Delphi and I am having trouble implementing a PATCH endpoint and I wondered if anyone has had the same issue as me.

If i have a simple resource Person which looks like this.

{
  "firstName": "John",
  "lastName": "Doe",
  "age": 44
}

and simply want to change his lastName property using a PATCH document - I would sent a request that looks like this:

{
  "lastName": "Smith"
}

Now - in my api, using Delphis System.JSON library I would just check if the request has the firstName and age properties before setting them in the request handler which sets the properties in an intermediate object PersonDTO, but later I have to map these values to the actual Person instance - and here comes my issue:

When mapping between multiple objects I cannot tell if a string is empty because it was never set (and should be treated as null) or was explicitly set to '' to remove a property from my resource - How do I circumvent this?

 if personDTO.FirstName <> '' then
   personObject.FirstName := personDTO.FirstName;

Edit: I have considered setting the strings to #0 in the DTO's constructor to distinguish between null and '' but this is a large (1M line) code base, so I would prefer to find a robust generic way of handling these scenarios

CodePudding user response:

Delphi does not differentiate between an empty string and an unassigned string. They are implemented the exact same way - as a nil pointer. So, you will have to use a different type that does differentiate, such as a Variant. Otherwise, you will have to carry a separate boolean/enum flag alongside the string to indicate its intended state. Or, wrap the string value inside of a record/class type that you can set a pointer at when assigned and leave nil when unassigned.

CodePudding user response:

in Delphi String is an array, but it's an array a little longer than the actual count of characters. For exemple in Delphi string always end up with the #0 at high(myStr) 1. this is need when casting the string to pchar. if in your flow you don't plan to cast the string to pchar then you can write a special char in this "invisible" characters to distinguish between null and empty (actually i never tested this solution)

CodePudding user response:

The answer is in your question itself. You need to know what has been supplied. This means that you either need to use what was actually provided to the API rather than serialising into an object (which has to include all the members of the object), or you need to serialise into an object whose members will support you knowing whether they have been set or not.

If you are serialising into an intermediate object for the API then when you come to update your actual application object you can use an assign method that only sets the members of the application object that were set in the API. Implementing these checks in the intermediate object for your API means that you won't have to change any code in the main application.

Code that suggests how you might do this:

unit Unit1;

interface

uses  Classes;

  type
  TAPIIVariableStates = (APIVarSet, APIVarIsNull);
  TAPIVariableState = Set of TAPIIVariableStates;
  TAPIString =class(TObject)
  protected
    _szString:          String;
    _MemberState:       TAPIVariableState;

    function  _GetHasBeenSet(): Boolean; virtual;
    function  _GetIsNull(): Boolean; virtual;
    function  _GetString(): String; virtual;
    procedure _SetString(szNewValue: String); virtual;

  public
    procedure  AfterConstruction(); override;

    procedure  Clear(); virtual;
    procedure  SetToNull(); virtual;

    property  Value: String read _GetString write _SetString;
    property  HasBeenSet: Boolean read _GetHasBeenSet;
    property  IsNull: Boolean read _GetIsNull;
  end;

  TAPIPerson = class(TPersistent)
  protected
    FFirstName:         TAPIString;
    FLastName:          TAPIString;
    FComments:          TAPIString;

    procedure AssignTo(Target: TPersistent); override;

    function  _GetComments(): String; virtual;
    function  _GetFirstName(): String; virtual;
    function  _GetLastName(): String; virtual;
    procedure _SetComments(szNewValue: String); virtual;
    procedure _SetFirstName(szNewValue: String); virtual;
    procedure _SetLastName(szNewValue: String); virtual;

  public
    destructor Destroy; override;
    procedure AfterConstruction(); override;

    property  FirstName: String read _GetFirstName write _SetFirstName;
    property  LastName: String read _GetLastName write _SetLastName;
    property  Comments: String read _GetComments write _SetComments;

  end;

  TApplicationPerson = class(TPersistent)
  protected
    FFirstName:         String;
    FLastName:          String;
    FComments:          String;
  public
    property  FirstName: String read FFirstName write FFirstName;
    property  LastName: String read FLastName write FLastName;
    property  Comments: String read FComments write FComments;
  end;

implementation

uses  SysUtils;

  destructor TAPIPerson.Destroy();
  begin
    FreeAndNil(Self.FFirstName);
    FreeAndNil(Self.FLastName);
    FreeAndNil(Self.FComments);
    inherited;
  end;

  procedure TAPIPerson.AfterConstruction();
  begin
    inherited;
    Self.FFirstName:=TAPIString.Create();
    Self.FLastName:=TAPIString.Create();
    Self.FComments:=TAPIString.Create();
  end;

  procedure TAPIPerson.AssignTo(Target: TPersistent);
  begin
    if(Target is TApplicationPerson) then
    begin
      if(Self.FFirstName.HasBeenSet) then
        TApplicationPerson(Target).FirstName:=Self.FirstName;
      if(Self.FLastName.HasBeenSet) then
        TApplicationPerson(Target).LastName:=Self.LastName;
      if(Self.FComments.HasBeenSet) then
        TApplicationPerson(Target).Comments:=Self.Comments;
    end
    else
      inherited;
  end;

  function TAPIPerson._GetComments(): String;
  begin
    Result:=Self.FComments.Value;
  end;

  function TAPIPerson._GetFirstName(): String;
  begin
    Result:=Self.FFirstName.Value;
  end;

  function TAPIPerson._GetLastName(): String;
  begin
    Result:=Self.FLastName.Value;
  end;

  procedure TAPIPerson._SetComments(szNewValue: String);
  begin
    Self.FComments.Value:=szNewValue;
  end;

  procedure TAPIPerson._SetFirstName(szNewValue: String);
  begin
    Self.FFirstName.Value:=szNewValue;
  end;

  procedure TAPIPerson._SetLastName(szNewValue: String);
  begin
    Self.FLastName.Value:=szNewValue;
  end;

  procedure TAPIString.AfterConstruction();
  begin
    inherited;
    Self._MemberState:=[APIVarIsNull];
  end;

  procedure TAPIString.Clear();
  begin
    Self._szString:='';
    Self._MemberState:=[APIVarIsNull];
  end;

  function TAPIString._GetHasBeenSet(): Boolean;
  begin
    Result:=(APIVarSet in Self._MemberState);
  end;

  function TAPIString._GetIsNull(): Boolean;
  begin
    Result:=(APIVarIsNull in Self._MemberState);
  end;

  function TAPIString._GetString(): String;
  begin
    Result:=Self._szString;
  end;

  procedure TAPIString._SetString(szNewValue: String);
  begin
    Self._szString:=szNewValue;
    Include(Self._MemberState, APIVarSet);
    (* optionally treat an emoty strung and null as the same thing
    if(Length(Self._szString)=0) then
      Include(Self._MemberState, APIVarIsNull)
    else
      Exclude(Self._MemberState, APIVarIsNull); *)
  end;

  procedure TAPIString.SetToNull();
  begin
    Self._szString:='';
    Self._MemberState:=[APIVarSet, APIVarIsNull];
  end;

end.

Using AssignTo in the TAPIPerson means that if your TApplicationPerson object derives from TPersistent (and has a properly implemented Assign method) then you can just use <ApplicationPersonObject>.Assign(<APIPersonObject>) to update just those fields which have changed. Otherwise you need a public method in the TAPIPerson that will update the TApplicationPerson appropriately.

  • Related