Jump to content
Lars Fosdal

I wish I had RTTI for constants and/or a compiler magic NameOf()

Recommended Posts

< wishful >

I keep running into cases where I have to manually duplicate the name of a constant to register it for lookup, or associate the constant with a numeric ID which then is used for the lookup.

Either method means double book keeping and is error prone.

 

Some of it would be nicely handled by having a compiler magic NameOf method that could give me name of a constant, field or property as a string at compile time.

 

In other cases it would have been awesome to be able to enumerate a class or a record and get the declared constant names, types and values.  This particularly goes for types I want to document for f.x. a JsonRPC API.

Instead, I have to manually emit that information, constant for constant, which means someone will eventually forget to do that.

 

If only the compiler gods where listening...

 

< / wishful >

Share this post


Link to post

have a look at the TRttiEnumerationType  class in RTTI.pas

you can do like this

type
TmyEnum=(enum1, enum2);

TRttiEnumerationType.GetName<TmyEnum>(enum1) >> returns 'enum1'  as a string
TRttiEnumerationType.GetValue<TmyEnum>('enum1') >> returns the enum1

quite ugly... but here you go.
Usually I write a record helper to simplyfy it a bit like this:

TmyEnumHelper = record helper for TMyEnum
function toStr:string;
class function from(const aName:String): TmyEnum; static;
end;

function TmyEnumHelper.toStr:string;
begin
  result:= TRttiEnumerationType.GetName<TmyEnum>(self);
end;

class function TmyEnumHelper.from(const aName: String): TmyEnum; static;
begin
  result:= TRttiEnumerationType.GetValue<TmyEnum>(aName);
end;

this simplifies it a bit, and then you can use it like this:

var e:TMyEnum;
...
e:= TmyEnum.from('enum1');

// and later
e.toStr; >> gives 'enum1';

you can also just cast it to a integer value

var
i:integer;
e:TmyEnum;
...
i:=ord(enum1);
e := TmyEnum(i);
Edited by Pawel Piotrowski

Share this post


Link to post

Wrong approach - use a parser to generate doc out of some code.

 

Agree on the nameof intrinsic though - but for other reasons.

Edited by Stefan Glienke

Share this post


Link to post

Still doesn't help you for something like this:

 

procedure p(x,y: Byte);
begin
	if (y = 0) then
		raise EArgumentOutOfRangeException.Create( NameOf(y) + ' must not be zero');
	(...)
end;

 

Share this post


Link to post

Keep voting: https://quality.embarcadero.com/browse/RSP-13290

 

And given that the compiler already knows line numbers and the filepath it's working on for Assert it should be almost no effort to implement something like this as well:

https://docs.microsoft.com/en-gb/dotnet/api/system.runtime.compilerservices.callermembernameattribute?view=netframework-4.7.1

Edited by Stefan Glienke
  • Like 2

Share this post


Link to post
 
 
 
 
4 hours ago, Pawel Piotrowski said:
 
 
 
 
3 hours ago, Pawel Piotrowski said:

have a look at the TRttiEnumerationType  class in RTTI.pas

 can do like this

 

Pawel, Enums are not the problem.   Using helper classes, these are trivial to deal with.

Constants that are strings or records are, as is the fact that constants don't have RTTI info.


 

4 hours ago, Stefan Glienke said:

Wrong approach - use a parser to generate doc out of some code.

Stefan, the problem with the parser is that it would be near impossible to catch everything since we are talking about a class hierarchy with multiple inheritance branches, and I'd spend more time tinkering with the parser, than manually maintaining explicit output. The current approach is not perfect, but it does make it nearly automatic to keep the API docs up to date and document the source code at the same time.  The best formal way would probably be to define it all in Swagger, but ... yeah... not intended for public consumption.

An example of how I use RTTI to produce documentation from types and attributes to the various fields, here is a sample of one of the API calls for one of our JsonRPC servers, which is generated (once) at runtime from code. All Json samples are generated using the actual objects, so that changes are reflected whenever something is added/removed/changed.  

 

procedure TDoc_TPGMission.Login;
begin
  Method(TLoginRequest);
  BuildExample<TLoginRequest, TLoginResult>(
    procedure (const Json:TLoginRequest)
    begin
      Json.params.step := TPGStep.S01_Login.AsString;
      Json.params.username := 'picavi';
      Json.params.password := ' ... ';
      Json.params.clientId := 'Glass-1';
      Json.params.deviceType := 'Glass';
      Json.params.truckId := 'T011';
    end,
    procedure (const Json: TLoginResult)
    begin
      Json.sessionIdent := SessIdent;
      json.configuration.language := 'NO';
      json.configuration.mode := 'professional-superuser';
    end, ['Fully qualified login. See below for partially qualified logins.'],
      -32990, 'Login failed', 'Wrong username [<username>]');

  BuildExample<TLoginRequest, TLoginResult>(
    procedure (const Json:TLoginRequest)
    begin
      Json.params.step := TPGStep.S01_Login.AsString;
      Json.params.clientId := 'Glass-1';
    end,
    procedure (const Json: TLoginResult)
    begin
      Json.sessionIdent := SessIdent;
      json.userList.Add('usrid1', 'foslar', 'Lars Fosdal');
      json.userList.Add('usrid2', 'krujon', 'Jonas Krueckels');
      json.truckList.Add('T011', 'Truck 11', 'Four slot pick truck');
      json.truckList.Add('T012', 'Truck 12', 'Four slot pick truck');
      json.truckList.Add('ADIDAS', 'Walking', 'Picker walks without truck');
      json.missionList.Add('PICKX', 'X Pick', 'Regular picking at Area X');
      json.missionList.Add('PICKY', 'Y Pick', 'Regular picking at Area Y');
      json.missionList.Add('SMALL', 'Bag Picking', 'Pick small bag deliveries');
      json.Result := False;
    end, ['Partially qualified login. Missing id(s) will cause an error, but will instead return result.result as False.',
          'This means each of the lists has to be inspected to see what was missing',
          'and the appropriate input or selection must be done by the user.',
          'As long as the login request is missing information, it will "loop" until the login is qualified.',
          'Erroneous information should yield an error.'],
      -32990, 'Login failed', 'Wrong username [<username>]');

end;

 

image.thumb.png.fc224b2978ce3acd376521136bdde6a8.png

 

I use attributes in the Json objects to add a short description of each property.

 

  TLoginParams = class(TSessionParams)
  strict private
    FdeviceType: string;
    FclientId: string;
    Fusername: string;
    [Encrypted]
    Fpassword: string;
    FmissionId: string;
    Flanguage: string;
    Fmode: string;
    Fprotocol: integer;
    FareaId: string;
  public
    class function RPCName:String; override;
    class function PropDoc(const aPropName: string): string; override;

    constructor Create; override;
    function DeviceClient:String;
    [Doc('Device Type')]
    property deviceType: string read FdeviceType write FdeviceType;

    [Doc('Client identifier ')]
    property clientId: string read FclientId write FclientId;

    [Doc('User short name')]
    property username: string read Fusername write Fusername;

    [Doc('Password')]
    property password: string read Fpassword write Fpassword;

    [Doc('UI Language, supported languages: "EN" for English, "NO" for Norwegian, "SE" for Swedish')]
    property language: string read Flanguage write Flanguage;

    [Doc('UI Mode - deprecated - should always be "professional"')]
    property mode: string read Fmode write Fmode; // 'Professional' 'Normal' 'Professional-SuperUser' 'Normal-SuperUser'

    [Doc('Mission Identity')]
    property missionId: string read FmissionId write FmissionId;

    [Doc('Pick area identity (Pick zone)')]
    property areaId: string read FareaId write FareaId;

    [Doc('Supported device protocol level')] /// <summary> Supported device protocol level </summary>
    property protocol: Integer read Fprotocol write Fprotocol;
  end;
  TLoginRequest = class(TJsonRPCRequest<TLoginParams>);


One exception is an Error code table, which has to be handcrafted and manually updated, since the numerical error codes must be constant.

Here is an excerpt:

 TDispatchError = record
  // NB! Update procedure TDoc_TPGMission.ErrorCodes in TPG.JsonRPC.Dispatcher when adding error codes
  private const
    LoginError     = -32900;
    TUError        = -33100;
    LocationError  = -33200;
    ArticleError   = -33300;
    SaldoError     = -33400;
    MissionError   = -33500;
    SystemError    = -33600;
    MethodError    = -33700;
    SwapError      = -33800;
    DBError        = -40000;
  public const
    UnknownSession          = LoginError - 1;
    NoDeviceType            = LoginError - 2;
    UnsupportedDeviceType   = LoginError - 3;
    NoDevice                = LoginError - 4;
    InvalidDevice           = LoginError - 5;
    InvalidTruck            = LoginError - 6;
    InvalidMission          = LoginError - 7;
    MethodNotImplemented    = LoginError - 8;

    InvalidArea             = LoginError - 70;

    OtherUserHasTruck       = LoginError - 80;
    LoggedIntoAnotherTruck  = LoginError - 81;

    InvalidUser             = LoginError - 90;
    UserAlreadyLoggedIn     = LoginError - 91;

    CallSuperUser           = LoginError - 93;
    InvalidPassword         = LoginError - 94;
    InvalidPINCode          = LoginError - 95;

    StoragePositionFull     = TUError - 1;
    TUNotFound              = TUError - 2;
    MultipleTUsFound        = TUError - 3;

    OrderSave               = MissionError - 1;
    OrderLineSave           = MissionError - 2;
    OrderRead               = MissionError - 3;
    OrderLineRead           = MissionError - 4;
    AlreadyAllocated        = MissionError - 5;

This is where I wish I could use NameOf(TDispatchError.Whatever) to get 'Whatever" and put attributes in front of the constant to add a description that I could extract with RTTI.

 

procedure TDoc_TPGMission.ErrorCodes;
begin
  Chapter('TPG Error Codes ', 'Error_Codes', 1);
  Title := 'All error codes are negative numbers and they are grouped into the following ranges.';
  DocTableBegin(html.divStyle.detail);
  DocTableHeaders(['Error name', 'value', 'description']);
  DefTitle('Login Errors');
    Def('UnknownSession', TDispatchError.UnknownSession);
    Def('NoDeviceType', TDispatchError.NoDeviceType);
    Def('UnsupportedDeviceType', TDispatchError.UnsupportedDeviceType);
    Def('NoDevice', TDispatchError.NoDevice);
    Def('InvalidDevice', TDispatchError.InvalidDevice);
    Def('InvalidTruck', TDispatchError.InvalidTruck);
    Def('InvalidMission', TDispatchError.InvalidMission);
    Def('MethodNotImplemented', TDispatchError.MethodNotImplemented);
    Def('InvalidArea', TDispatchError.InvalidArea);

 

image.thumb.png.758af29351933750c56587ba9d1e505d.png

 

The TOC
image.thumb.png.8992981b4c57f7fd4785665ef5061289.png

Share this post


Link to post

TL'DR 😛

 

Btw you can utilize the xmldoc for that - we are doing that for some of our classes and pump that into confluence.

Does not need a single line of executable code or RTTI inside the to be documented to produce this. The only disadvantage this has is that implementation and what's in the xmldoc might differ.

Share this post


Link to post

Well, while this feature isn't supported by compiler, you could resolve it by auto-generating. Create a file with error definitions and descriptions and a script that would generate pascal source out of it.

Edited by Fr0sT.Brutal
  • Like 1

Share this post


Link to post

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

×