Lars Fosdal 1792 Posted September 26, 2019 < 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
Pawel Piotrowski 18 Posted September 26, 2019 (edited) 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 September 26, 2019 by Pawel Piotrowski Share this post Link to post
Stefan Glienke 2002 Posted September 26, 2019 (edited) Wrong approach - use a parser to generate doc out of some code. Agree on the nameof intrinsic though - but for other reasons. Edited September 26, 2019 by Stefan Glienke Share this post Link to post
Der schöne Günther 316 Posted September 26, 2019 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
Stefan Glienke 2002 Posted September 26, 2019 (edited) 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 September 26, 2019 by Stefan Glienke 2 Share this post Link to post
Lars Fosdal 1792 Posted September 26, 2019 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; 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); The TOC Share this post Link to post
Stefan Glienke 2002 Posted September 26, 2019 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
Lars Fosdal 1792 Posted September 26, 2019 I considered XMLDoc, but compile and runtime validation won out. Share this post Link to post
Fr0sT.Brutal 900 Posted September 27, 2019 (edited) 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 September 27, 2019 by Fr0sT.Brutal 1 Share this post Link to post