Introduzione a WiRLClient
Tags: delphi, rest, wirl
Published: 2024-01-29 07:16:31

Spesso [WiRL](https://github.com/delphi-blocks/WiRL) viene usato solo come una libreria per scrivere API ReST. Ma già dalle prime versioni è presente un modulo per la creazione di client ReST che si possono adattare a server scritti con **qualunque tecnologia**.

Negli ultimi tempi abbiamo cercato di semplificare al massimo l'uso di questa parte della libreria lasciandone inalterata la flessibilità e anzi cercando, dove possibile, di ampliarne i possibili casi d'uso. Parte di questo processo consiste nel utilizzare gli **stessi concetti dello sviluppo del server** anche per la parte client. In effetti molte classi presenti nel server si ripresentano in modo analogo sul client:

| Server               | Client                    |
| -------------------- |:-------------------------:|
| TWiRLServer          | TWiRLClient               | 
| TWiRLApplication     | TWiRLClientApplication    |
| Risorse (any class*) | TWiRLClientResource       |
| TWiRLFilterRegistry  | TWiRLClientFilterRegistry |
| TMessageBodyReader** | TMessageBodyReader        |
| TMessageBodyWriter** | TMessageBodyWriter        |
\* Qualunque classe registrata con `RegisterResource`
\** Usano gli stessi reader e writer!!

## TWiRLClient

La classe base che implementa il protocollo HTTP è `TWiRLClient`; l'utilizzo è molto semplice e ricorda quello della classe [THTTPClient](http://docwiki.embarcadero.com/Libraries/Alexandria/en/System.Net.HttpClient.THTTPClient) (presente da DelphiXE8). Questa classe è il cuore della parte client di WiRL e le varie classi come `TWiRLClientApplication` e `TWiRLClientResource` la usano per comunicare col server. 

```pascal
procedure TForm1.Button1Click(Sender: TObject);
var
  LResponse: IWiRLResponse;
  LHeader: TWiRLHeader;
  LRequestHeader: IWiRLHeaders;
begin
  LRequestHeader := TWiRLHeaders.Create;
  LRequestHeader.Accept := 'application/json';
  LResponse := WiRLClient1.Get('http://localhost:9999/', nil, LRequestHeader);
  Memo1.Lines.Add(LResponse.ContentText);
end;
```

Nell'esempio al metodo `Get` viene passato l'**URL** da richiamare, uno **stream** da usare per il corpo della risposta (se è _nil_, come nell'esempio, viene creato un _TMemoryStream_ da WiRL che sarà anche distrutto in automatico insieme all'oggetto _IWiRLResponse_) e la lista degli **header**.

L'oggetto TWiRLClient, per effettuare la chiamate a sua volta si appoggia a THTTP (Indy) oppure THttpClient (nativo Delphi con supporto HTTPS). Si può scegliere quale usare tramite la proprietà `ClientVendor`. **Attenzione**: a seconda del vendor scelto deve essere inclusa la unit `WiRL.http.Client.Indy` o `WiRL.http.Client.NetHttp`.

È anche possibile creare delle implementazioni personalizzate basate per esempio su [ICS](http://www.overbyte.be/frame_index.html) o [Synapse](https://sourceforge.net/projects/synalist/) semplicemente implementando le interfacce `IWiRLClient` e `IWiRLResponse`.

Inoltre il componente possiede la proprietà `WiRLEngineURL` che viene usata da `TWiRLClientApplication` e `TWiRLClientResource` come *entry point* base le chiamate HTTP(S).

## TWiRLClientApplication

Come sul server la classe `TWiRLClientApplication` serve per configurare message body reader e writer, i filtri e tutti i plugin registrati. Il modo più semplice per inizializzarla è tramite la [fluent interface](https://www.martinfowler.com/bliki/FluentInterface.html):

```pascal
constructor TMyClass.InitWiRLClient;
begin
  // Crea l'istanza di TWiRLClientApplication
  FApp := TWiRLClientApplication.Create(nil);
  FApp
    // Associa tutti i reader registrati
    .SetReaders('*.*')
    // Associa tutti i writer registrati
    .SetWriters('*.*')
    // Associa tutti i filtri registrati
    .SetFilters('*.*')
    // Configurara Neon
    .Plugin.Configure<IWiRLConfigurationNeon>
      .SetUseUTCDate(True)
      .SetVisibility([mvPublic, mvPublished])
      .SetMemberCase(TNeonCase.CamelCase)
      .BackToApp;

  FApp.AppName := 'app';
  FApp.Client := FClient;
  // ...
end;
```

I  **message body reader e writer** sono gli stessi del server e permettono di trasformare gli stream di dati inviati al server o in arrivo dal server in oggetti. In questo modo uno stream con un [JSON](https://www.json.org/json-it.html) come questo:

```json
{
  "name": "Luca",
  "role": "ADMIN"
}
```
può essere usato per inizializzare un ipotetico oggetto _TUser_.

I **filtri** possono essere usati un po' come degli eventi per intercettare la richiesta inviata al server o la risposta e compiere qualche azione (eseguire un log, gestire l'autenticazione, salvare i cookie, ecc.).

## TWiRLClientResource

Una volta inizializzati TWiRLClient e TWiRLClientApplication è possibile effettuare le chiamate ReST. Anche in questo caso è possibile usare la sintassi *fluent* e visto che l'implementazione è fatta tramite interfacce non è necessario distruggere gli oggetti di WiRL.

```pascal
function TMainDataModule.GetPerson(Id: Integer): TPerson;
begin
  Result := WiRLClientApplication1
    // Crea l'oggetto invokation per la risorsa
    .Resource('person')
    // Imposta l'header accept (necessario al server
    // per decidere il formato della risposta)
    .Accept('application/json')
    // Imposta il parametro *Id*
    .QueryParam('Id', Id.ToString)
    // Effettua la chiamata deserializando la risposta 
    // in un oggetto di tipo TPerson
    .Get<TPerson>;
end;
```

Con questo esempio viene chiesto a *WiRLClientApplication1* di generare una nuova `TWiRLInvocation` sulla risorsa indicata. Attraverso i metodi `Accept` e `QueryParam` si impostano gli header e i parametri richiesti dal server. Il metodo `Get` farà partire la richiesta vera e propria e il corpo del messaggio restituito dal server sarà usato per creare un oggetto di tipo `TPerson` come indicato.

### Oggetti creati dall'utente

Un'altra modalità d'uso è quella di passare al metodo `Get` (ma la stessa cosa vale anche per `Post`, `Put`, `Delete`, ecc.) un oggetto creato da noi che verrà "riempito" con i dati forniti dal server.

```pascal
function TMainDataModule.GetPersonName(Id: Integer): string;
var
  LPerson: TPerson;
begin
  LPerson := TPerson.Create;
  try
    WiRLClientApplication1
      .Resource('person')
      .Accept('application/json')
      .QueryParam('Id', Id.ToString)
      .Get(LPerson);
    Result := LPerson.Name;
  finally
    LPerson.Free;
  end;
end;
```

Questo tipo di approccio è particolarmente utile del caso il server restituisca un *array* di oggetti. Se le proprietà degli oggetti sono equivalenti ai campi di un *dataset* WiRL è in grado di popolare i campi del DataSet (per esempio una memory table) automaticamente.

```pascal
procedure TMainDataModule.GetPeople(ADataSet: TDataSet);
begin
  WiRLClientApplication1
    .Resource('person')
    .Accept('application/json')
    .Get(ADataSet);
end;
```
### Leggere la risposta a basso livello

In alcuni casi può essere utile leggere la risposta del server in maniera più dettagliata. Per esempio il server potrebbe restituire un *JSON* fatto in un modo o in un altro a seconda di determinati *header* o dello [status code](https://en.wikipedia.org/wiki/List_of_HTTP_status_codes). In questo caso è necessario accedere a `IWiRLResponse` che contiene tutte le informazioni sulla risposta.

```pascal
function TMainDataModule.GetPerson(Id: Integer): TPerson;
var
  LResponse: IWiRLResponse;
begin
  LResponse := WiRLClientApplication1
    .Resource('person')
    .Accept('application/json')
    .QueryParam('Id', Id.ToString)
    .Get<IWiRLResponse>;

  // Fa qualcosa con la response
  if LResponse.Headers.ContentType <> 'application/json' then
    Abort;

  Result := LResponse.Content.AsType<TPerson>();
end;
```

### Gestione delle eccezioni

In caso di errori di protocollo (status code 400 o 500) la chiamate al server sollevano un eccezione di tipo `EWiRLClientProtocolException` questo oggetto, oltre al messaggio di errore, contiene l'intera risposta del server (nella property `Response`); è quindi possibile andare a recuperare qualsiasi informazione restituita dal server.

```pascal
procedure TMainDataModule.GetPeople(ADataSet: TDataSet);
begin
  try
    WiRLClientApplication1
      .Resource('person')
      .Accept('application/json')
      .Get(ADataSet);
  except
    on E: EWiRLClientProtocolException do
    begin
      Log.Error('ERROR: ' + E.Response.ContentText);
      raise;
    end;
  end;
end;
```
In alternativa è possibile disabilitare le eccezioni e gestire lo *status code* manualmente.

```pascal
procedure TMainDataModule.GetPeople(ADataSet: TDataSet);
var
  LResponse: IWiRLResponse;
begin
  LResponse := WiRLClientApplication1
    .Resource('person')
    // Disabilita le eccezioni di protocollo 
    .DisableProtocolException
    .Accept('application/json')
    .Get<IWiRLResponse>(IWiRLResponse);

  if LResponse.Status = TWiRLResponseStatus.Success then
    LResponse.Content.AsType(ADataSet);

end;
```

Nell'esempio è stata usata la proprietà `Status` della risposta che contiene un enumerativo che indica la categoria dello status code HTTP (Informational, Success, Redirect, ClientError, ServerError);

## Altro

Ci sono molte altre funzionalità utili di *WiRL Client* come:

* La gestione dei filtri
* Autenticazione e autorizzazione
* Personalizzazione di message body reader e writer
* Creazioni di nuovi "vendor" per TWiRLClient
* Chiamate asincrone

Nei prossimi articoli vedremo di approfondire tutti questi temi.