Skip to content

viniciusfbb/ipub-refit

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

29 Commits
 
 
 
 
 
 

Repository files navigation

iPub Refit

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'));

Using

Interface

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).

Methods

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.

Methods arguments

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;

Base Url

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');

Headers

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;

Properties

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;

Authentication

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.

Functional example

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;

Using json attributes

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;

Registering a custom json converter

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.

Nullable types

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

Cancel requests

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.

Handling exceptions

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.

Try functions

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.

Considerations

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.

Compatibility

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.

License

The iPub Refit is licensed under MIT, and the license file is included in this folder.