The iPub Refit is a library to consume REST services in a very simple way, declaring only one interface and is created by the iPub team.
You don't have to deal with string manipulation, url encoding, json manipulation, client settings. This is all done automatically by this library, you just need to declare the header of the api rest that will be consumed.
This project inspired / based on the existing Refit in .Net, and it turns your REST API into a live interface:
TUser = record
name: string;
location: string;
id: Integer;
end;
[BaseUrl('https://api.github.com')]
IGithubApi = interface(IipRestApi)
['{4C3B546F-216D-46D9-8E7D-0009C0771064}']
[Get('/users/{aUser}')]
function GetUser(const AUser: string): TUser;
[Get('/users/{aUser}')]
function GetUserJson(const AUser: string): string;
end;
The GRestService instance generates an implementation of IGitHubApi that internally uses TNetHTTPClient to make its calls:
var
LGithubApi: IGithubApi;
LUser: TUser;
begin
LGithubApi := GRestService.&For<IGithubApi>;
LUser := LGithubApi.GetUser('viniciusfbb');
Showmessage(LGithubApi.GetUserJson('viniciusfbb'));
To declare the rest api interface, there are two obligations:
- The interface must be descendent of the IipRestApi or be declared inside a {$M+} directive.
- The interface must have one IID (GUID).
The methods of the rest api interface can be a procedure or a function returning a string, record, class, dynamic array of record or dynamic array of class. The method name don't matter. To declare you should declare an attribute informing the method kind and relative url You should declare the method kind of the interface method and the relative url
[Get('/users/{AUser}')]
function GetUserJson(const AUser: string): string;
[Post('/users/{AUser}?message={AMessage}')]
function GetUserJson(const AUser, AMessage: string): string;
All standard methods are supported (Get, Post, Delete, Put and Patch).
The relative url can have masks {argument_name/property_name}, in anywhere and can repeat, to mark where an argument or property can be inserted. More details in the next topic.
In your rest api interface, the arguments name of methods will be used to replace the masks {argument_name/property_name} in relative url. In this step we permit case insensitive names and names without the first letter A of argument names used commonly in delphi language. So, this cases will have the same result:
[Get('/users/{AUser}')]
function GetUser(const AUser: string): TUser;
[Get('/users/{aUser}')]
function GetUser(const AUser: string): TUser;
[Get('/users/{User}')]
function GetUser(const AUser: string): TUser;
[Get('/users/{user}')]
function GetUser(const AUser: string): TUser;
If the argument name is Body
, ABody
, BodyContent
, ABodyContent
, Content
or AContent
, the argument will be used as the body of the request. You can also declare other name and use the attribute [Body] in this argument. When a argument is a body, no matter the argument type, it will be casted to string. If it is a record or class, we will serialize it to a json automatically.
Remember that the mask {argument_name/property_name} in relative url, can be in anywhere, including inside queries, and can repeat. In addition it will be automatically encoded, so your argument can be a string with spaces, for example.
The type of the argument don't matter, we will cast to string automatically. So you can use, for example, an integer parameter:
[Get('/users/{AUserId}')]
function GetUser(const AUserId: Integer): string;
You can declare optionally the base url using the attribute [BaseUrl('xxx')] before the interface:
[BaseUrl('https://api.github.com')]
IGithubApi = interface(IipRestApi)
['{4C3B546F-216D-46D9-8E7D-0009C0771064}']
end;
Or you can set directly when the rest service generate a new rest api interface, this is the first base url that the service will consider:
LGithubApi := GRestService.&For<IGithubApi>('https://api.github.com');
You can declare the headers necessary in the api interface and method. To the declare headers that will be used in all api call, just declare above the interface:
[Headers('User-Agent', 'Awesome Octocat App')]
[Headers('Header-A', '1')]
IGithubApi = interface(IipRestApi)
['{4C3B546F-216D-46D9-8E7D-0009C0771064}']
end;
To the declare headers that will be used in one api method, just declare above the method:
IGithubApi = interface(IipRestApi)
['{4C3B546F-216D-46D9-8E7D-0009C0771064}']
[Headers('Header-A', '1')]
[Get('/users/{AUser}')]
function GetUserJson(const AUser: string): string;
end;
Note: you can declare many [Headers] attribute in one method or in one rest api interface.
But to declare dynamic headers, that is, depending on arguments or properties, you can also use the {argument_name/property_name} mask on the header value. But there is also another exclusive option to declare a header using an argument directly. In this last case, you will need to declare the [Header] attribute in the parameter declaration:
IGithubApi = interface(IipRestApi)
['{4C3B546F-216D-46D9-8E7D-0009C0771064}']
[Post('/users/{AUser}')]
function GetUserJson(const AUser: string; [Header('Authorization')] const AAuthToken: string): string;
end;
In your rest api interface, you can also declare any kind of property, as long as it is for reading and writing and has getter and setter with the same name as the property just by adding "Get" and "Set" beforehand. Example:
[BaseUrl('https://maps.googleapis.com')]
IipGoogleGeocoding = interface(IipRestApi)
['{668C5505-F90D-43C0-9545-038A86472666}']
[Get('/maps/api/geocode/json?address={AAddress}&key={ApiKey}')]
function AddressToGeolocation(const AAddress: string): string;
[Get('/maps/api/geocode/json?latlng={ALatitude},{ALongitude}&key={ApiKey}')]
function GeolocationToAddress(const ALatitude, ALongitude: Double): string;
function GetApiKey: string;
procedure SetApiKey(const AValue: string);
property ApiKey: string read GetApiKey write SetApiKey;
end;
procedure TForm1.FormCreate(Sender: TObject);
var
LGoogleGeocodingApi: IipGoogleGeocoding;
LGeolocationResponse: string;
LAddressResponse: string;
begin
LGoogleGeocodingApi := GRestService.&For<IipGoogleGeocoding>;
LGoogleGeocodingApi.ApiKey := 'XXXXXXX';
LGeolocationResponse := LGoogleGeocodingApi.AddressToGeolocation('Rua Alexandre Dumas, 1562, Chácara Santo Antônio, São Paulo - SP, Brasil');
LAddressResponse := LGoogleGeocodingApi.GeolocationToAddress(-23.6312393, -46.7058503);
end;
When you need some kind of authenticator like OAuth1 or OAuth2, you can use the native components of delphi like TOAuth2Authenticator. In this case you will need to create and configure this authenticator by your self and set it in property Authenticator of your rest api interface (it has been declared in parent interface, IipRestAPI)
var
LOAuth2: TOAuth2Authenticator;
LGithubApi: IGithubApi;
begin
LOAuth2 := TOAuth2Authenticator.Create(nil);
try
// Configure the LOAuth2
...
LGithubApi := GRestService.&For<IGithubApi>;
LGithubApi.Authenticator := LOAuth2;
Showmessage(LGithubApi.GetUserJson('viniciusfbb'));
finally
LOAuth2.Free;
end;
end;
Note: you need to destroy the authenticator by your self after use it, the rest service will not do it internally.
uses
iPub.Rtl.Refit;
type
TUser = record
Name: string;
Location: string;
Id: Integer;
end;
TRepository = record
Name: string;
Full_Name: string;
Fork: Boolean;
Description: string;
end;
TIssue = record
public
type
TUser = record
Login: string;
Id: Integer;
end;
TLabel = record
Name: string;
Color: string;
end;
public
Url: string;
Title: string;
User: TIssue.TUser;
Labels: TArray<TIssue.TLabel>;
State: string;
Body: string;
end;
[BaseUrl('https://api.github.com')]
IGithubApi = interface(IipRestApi)
['{4C3B546F-216D-46D9-8E7D-0009C0771064}']
[Get('/users/{user}')]
function GetUser(const AUser: string): TUser;
[Get('/users/{user}/repos')]
function GetUserRepos(const AUser: string): TArray<TRepository>;
[Get('/repos/{repositoryOwner}/{repositoryName}/issues?page={page}')]
function GetRepositoryIssues(const ARepositoryOwner, ARepositoryName: string; APage: Integer = 1): TArray<TIssue>;
[Get('/repos/{repositoryOwner}/{repositoryName}/issues?page={page}&state=open')]
function GetRepositoryIssuesOpen(const ARepositoryOwner, ARepositoryName: string; APage: Integer = 1): TArray<TIssue>;
end;
procedure TForm1.FormCreate(Sender: TObject);
var
LGithubApi: IGithubApi;
LUser: TUser;
LRepos: TArray<TRepository>;
LIssues: TArray<TIssue>;
begin
LGithubApi := GRestService.&For<IGithubApi>;
LUser := LGithubApi.GetUser('viniciusfbb');
LRepos := LGithubApi.GetUserRepos('viniciusfbb');
LIssues := LGithubApi.GetRepositoryIssues('rails', 'rails');
LIssues := LGithubApi.GetRepositoryIssuesOpen('rails', 'rails', 2);
end;
The types used in your interface can use the json attributes normally. All attributes of the unit System.JSON.Serializers are allowed. Example:
uses
iPub.Rtl.Refit, System.JSON.Serializers;
type
TRepository = record
Name: string;
[JsonName('full_name')] FullName: string;
Fork: Boolean;
Description: string;
end;
[BaseUrl('https://api.github.com')]
IGithubApi = interface(IipRestApi)
['{4C3B546F-216D-46D9-8E7D-0009C0771064}']
[Get('/users/{user}/repos')]
function GetUserRepos(const AUser: string): TArray<TRepository>;
end;
procedure TForm1.FormCreate(Sender: TObject);
var
LGithubApi: IGithubApi;
LRepos: TArray<TRepository>;
begin
LGithubApi := GRestService.&For<IGithubApi>;
LRepos := LGithubApi.GetUserRepos('viniciusfbb');
end;
If you have a special type, that need a custom convert, you can create your own json converter descendent of the TJsonConverter in System.JSON.Serializers and register it in us library:
GRestService.RegisterConverters([TNullableStringConverter]);
You will register just one time, preferably at initialization.
This library does not implement nullable types because there are several different implementations on the internet, several libraries already have their own nullable type. But with the possibility of registering custom json converters, it is easy to implement any type of nullable in the code to use together with this library. Here one example of one nullable type with the json converter:
unit ExampleOfNullables;
interface
uses
System.Rtti, System.TypInfo, System.JSON.Serializers, System.JSON.Readers,
System.JSON.Writers, System.JSON.Types, iPub.Rtl.Refit;
type
TNullable<T> = record
strict private
FIsNotNull: Boolean;
function GetIsNull: Boolean;
procedure SetIsNull(AValue: Boolean);
public
Value: T;
property IsNull: Boolean read GetIsNull write SetIsNull;
end;
implementation
type
TNullableConverter<T> = class(TJsonConverter)
public
procedure WriteJson(const AWriter: TJsonWriter; const AValue: TValue; const ASerializer: TJsonSerializer); override;
function ReadJson(const AReader: TJsonReader; ATypeInf: PTypeInfo; const AExistingValue: TValue;
const ASerializer: TJsonSerializer): TValue; override;
function CanConvert(ATypeInf: PTypeInfo): Boolean; override;
end;
{ TNullable<T> }
function TNullable<T>.GetIsNull: Boolean;
begin
Result := not FIsNotNull;
end;
procedure TNullable<T>.SetIsNull(AValue: Boolean);
begin
FIsNotNull := not AValue;
end;
{ TNullableConverter<T> }
function TNullableConverter<T>.CanConvert(ATypeInf: PTypeInfo): Boolean;
begin
Result := ATypeInf = TypeInfo(TNullable<T>);
end;
function TNullableConverter<T>.ReadJson(const AReader: TJsonReader;
ATypeInf: PTypeInfo; const AExistingValue: TValue;
const ASerializer: TJsonSerializer): TValue;
var
LNullable: TNullable<T>;
begin
if AReader.TokenType = TJsonToken.Null then
begin
LNullable.IsNull := True;
LNullable.Value := Default(T);
end
else
begin
LNullable.IsNull := False;
LNullable.Value := AReader.Value.AsType<T>;
end;
TValue.Make(@LNullable, TypeInfo(TNullable<T>), Result);
end;
procedure TNullableConverter<T>.WriteJson(const AWriter: TJsonWriter;
const AValue: TValue; const ASerializer: TJsonSerializer);
var
LNullable: TNullable<T>;
LValue: TValue;
begin
LNullable := AValue.AsType<TNullable<T>>;
if LNullable.IsNull then
AWriter.WriteNull
else
begin
TValue.Make(@LNullable.Value, TypeInfo(T), LValue);
AWriter.WriteValue(LValue);
end;
end;
initialization
GRestService.RegisterConverters([TNullableConverter<string>,
TNullableConverter<Byte>, TNullableConverter<Word>,
TNullableConverter<Integer>, TNullableConverter<Cardinal>,
TNullableConverter<Single>, TNullableConverter<Double>,
TNullableConverter<Int64>, TNullableConverter<UInt64>,
TNullableConverter<TDateTime>, TNullableConverter<Boolean>,
TNullableConverter<Char>]);
end.
Now, you can use the nullable with this library like:
uses
iPub.Rtl.Refit, ExampleOfNullables;
type
TUser = record
Name: TNullable<string>;
Location: string;
Id: Integer;
Email: TNullable<string>;
end;
[BaseUrl('https://api.github.com')]
IGithubApi = interface(IipRestApi)
['{4C3B546F-216D-46D9-8E7D-0009C0771064}']
[Get('/users/{user}')]
function GetUser(const AUser: string): TUser;
end;
// ...
var
LGithubApi: IGithubApi;
LUser: TUser;
begin
LGithubApi := GRestService.&For<IGithubApi>;
LUser := LGithubApi.GetUser('viniciusfbb');
// Now you will see that LUser.Name.IsNull = False but the LUser.Email.IsNull = True
It is important to be able to cancel any request that is being made, especially before closing the program, because waiting for the total execution can take a few seconds and thus delaying the closing of the program. For that, we created the CancelRequest method in api base class (IipRestApi), then just call:
LGithubApi.CancelRequest;
But this will raise an exception for the request in progress (EipRestServiceCanceled). More info in next topics.
We have five exceptions that need to be considered when using the service
- EipRestService: reserved for internal errors and wrong declarations of the rest api interface
- EipRestServiceCanceled: when you call one method of the api and another thread call the CancelRequest (no problem here, you just need to consider this)
- EipRestServiceFailed = when rest service fails like connection fail or protocol errors
- EipRestServiceJson = when the serialization / deserialization of json gives an error
- EipRestServiceStatusCode = when received a status code different then 2xx;
To "silence" the exceptions see the next topic.
To "silence" some exceptions you can declare "Try functions" in your rest api interface. Is very simple, just declare functions that starts with the "Try" name and return Boolean. If the method have a result you can also declare it in parameter with the "out" flag. See the same two methods declared with and without "Try function":
IGithubApi = interface(IipRestApi)
['{4C3B546F-216D-46D9-8E7D-0009C0771064}']
[Get('/users/{user}')]
function GetUser(const AUser: string): TUser;
[Get('/users/{user}')]
function TryGetUser(const AUser: string; out AResult: TUser): Boolean;
end;
Now, the TryGetUser will "silence" the exceptions EipRestServiceCanceled, EipRestServiceFailed, EipRestServiceJson and EipRestServiceStatusCode.
The GRestService and the rest api interfaces created by it are thread safe.
As the connections are synchronous, the ideal is to call the api functions in the background. If you have multiple threads you can also create multiple rest api interfaces for the same api, each one will have a different connection.
This library full cross-platform and was made and tested on delphi Sydney 10.4, but it is likely to work on previous versions, probably in Delphi 10.2 Tokyo or newer.
The iPub Refit is licensed under MIT, and the license file is included in this folder.