WiRL: Risorse e Attributi
Tags: delphi, rest, wirl
Published: 2024-02-19 08:34:54

Lo sviluppo di un server ReST con [WiRL](https://github.com/delphi-blocks/WiRL) consiste nel creare una serie di classi legate alle singole risorse esposte dalla nostra API. In genere ad una risorsa ReST corrisponde una classe e ad ogni metodo HTTP (GET, POST, PUT, DELETE, ecc.) un metodo della classe. Le cose possono essere più complicate nel caso una risorsa, a parità di metodo HTTP, preveda più risposte (per esempio in base al formato JSON, XML o altro); oppure in caso di sotto risorse (es. le fatture di un cliente: risorsa cliente, sottorisorsa fattura).

## Definizione della risorsa

Supponiamo di voler creare una API ReST che esponga una risorsa `user`. Inoltre vogliamo che la nostra risorsa sia accessibile sia in lettura che in scrittura. Seguendo l'approccio ReST dovremmo creare i seguenti *endpoint*:

```
# Legge i dati dell'utente tramite id
GET /api/user/{id}

# Restituisce i dati di una serie di utenti filtrati con diversi parametri
GET /api/user?name={name}&email={email}

# Modifica un utente (nel corpo del messaggio saranno presenti i dati dell'utente, per esempio in JSON)
PUT /api/user/{id}

# Aggiunge un utente
POST /api/user

# Elimina un utente tramite id
DELETE /api/user/{id}
```

## Creare la classe

Ora, se dovessimo creare una classe Delphi che rispecchi la risorsa, con un metodo per ogni `endpoint`, il modo più naturale per scriverla potrebbe essere il seguente: 

```pascal
  TUserResource = class(TObject)
  public
    // GET /api/user/{id}
    function GetUserById(AId: Integer): TUser;
    
    // GET /api/user?name={name}&email={email}
    function GetUser(const AName, AEmail: string): TObjectList<TUser>;

    // PUT /api/user/{id}
    function UpdateUser(AId: Integer; AUser: TUser): TUser;

    // POST /api/user
    function AppendUser(AUser: TUser): TUser;

    // DELETE /api/user/{id}
    function DeleteUser(AId: Integer): TUser;

  end;
```

Quello che permette di fare **WiRL** è proprio prendere quella classe, così com'è scritta, e renderla accessibile tramite *HTTP*. Ovviamente ci sono alcune cose che WiRL deve sapere prima di rendere possibile la pubblicazione delle API. Alcune sono relative alla configurazione generale dell'applicazione (la porta, il formato dei messaggi, ecc.) che sono stati spiegati in un [articolo precedente](https://dev.to/lminuti/wirl-tutorial-consoleapp-376), e poi ci sono le informazioni relative alla risorsa stessa.

Come si può vedere nella classe abbiamo inserito dei commenti che ci permettono di capire come i metodi della classe vengono associati ai metodi HTTP. Queste informazioni devono essere accessibili a *WiRL*, quello che dobbiamo fare è trasformare i commenti in una serie attributi messi a disposizione dalla libreria:

```pascal
  [Path('user')]
  TUserResource = class(TObject)
  public
    [GET]
    [Path('{id}')]
    [Produces(TMediaType.APPLICATION_JSON)]
    function GetUserById([PathParam('id')] AId: Integer): TUser;
    
    [GET]
    [Produces(TMediaType.APPLICATION_JSON)]
    function GetUser(
        [QueryParam('name')] const AName: string;
        [QueryParam('email')] const AEmail: string
    ): TObjectList<TUser>;

    [PUT]
    [Path('{id}')]
    [Consumes(TMediaType.APPLICATION_JSON)]
    [Produces(TMediaType.APPLICATION_JSON)]
    function UpdateUser(
        [PathParam('id')] AId: Integer; 
        [BodyParam] AUser: TUser
    ): TUser;

    [POST]
    [Consumes(TMediaType.APPLICATION_JSON)]
    [Produces(TMediaType.APPLICATION_JSON)]
    function AppendUser([BodyParam] AUser: TUser): TUser;

    [DELETE]
    [Path('{id}')]
    [Produces(TMediaType.APPLICATION_JSON)]
    function DeleteUser(PathParam('id')] AId: Integer): TUser;

  end;
```

In questo modo *WiRL* ha tutte le informazioni che gli servono.

## Gli attributi

Nella unit `WiRL.Core.Attributes` sono definiti parecchi attributi, qui vedremo solo quelli relativi alla definizione delle risorse. 

> **Attenzione**: se dimenticate di aggiungere la unit con la definizione degli attributi Delphi genererà un *Warning*: `W1074 Unknown custom attribute`. Il programma compilerà ugualmente ma non funzionerà. **Da Delph 10.3 potete trasformare il Warning in un errore delle opzioni di Delphi o per singolo progetto**. 

*Attributo:* `Path`

Questo attributo deve essere applicato ad una classe per fare un modo che WiRL la consideri una risorsa. Se viene applicato anche ad un metodo i due path vengono concatenati per formare una sotto risorsa. 

```pascal
  [Path('user')]
  TUserResource = class(TObject)
  public
    [GET]
    [Produces(TMediaType.APPLICATION_JSON)]
    function GetUser(...): ...;

    [GET]
    [Path('{id}/todo')]
    [Produces(TMediaType.APPLICATION_JSON)]
    function GetUserTodo(...): ...;

```

In questo esempio tramite il path `/user` sarà accessibile il metodo `GetUser`. Nel secondo esempio il path contiene un *template* `Path('{id}/todo')`. I template sono particolare stringhe che contengono delle parti variabili (in questo caso `id`) che sono accessibili al metodo tramite l'attributo `PathParam` spiegato in seguito. Questo metodo sarà quindi accessibile da URL come `/user/12/todo` o `/user/lminuti/todo`.

Tuttavia l'URL completo delle risorsa dipenderà anche dalla configurazioni impostata su `TWiRLServer`, `TWiRLEngine` e `TWiRLApplication` come spiegato nell'[articolo precedente](https://dev.to/lminuti/wirl-tutorial-consoleapp-376).

![Resource schema](https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2jw5q40vksh0sajt6cxe.png)

**Attributo:** `GET`

Deve essere applicato ad un metodo di classe. Indica che il metodo deve essere chiamato solo se il metodo HTTP è `GET`.  

**Attributo:** `PUT`

Deve essere applicato ad un metodo di classe. Indica che il metodo deve essere chiamato solo se il metodo HTTP è `PUT`.

**Attributo:** `POST`

Deve essere applicato ad un metodo di classe. Indica che il metodo deve essere chiamato solo se il metodo HTTP è `POST`.  

**Attributo:** `DELETE`

Deve essere applicato ad un metodo di classe. Indica che il metodo deve essere chiamato solo se il metodo HTTP è `DELETE`.  

**Attributo:** `Produces`

Come abbiamo visto in precedenza WiRL associa la risorsa al metodo di una classe in base agli attributi che indicano il metodo HTTP e il path. Il client però può anche indicare, tramite l'header [Accept](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept), in che formato vuole la risposta. L'attributo `Produces` deve indicare uno o più formati in cui il metodo è capace di restituire la risposta. 

> *Attenzione*: Questo attributo viene usato **solo** per fare il "match" tra richiesta e metodo. A questo punto WiRL eseguirà il codice del metodo che presumibilmente restituirà qualcosa in output. È solo al termine di questo processo che WiRL, attraverso i **message body writer**, tenterà di convertire l'output nel formato richiesto. In mancanza di un message body writer appropriato WiRL restituirà l'errore `415 - MediaType [%s] not supported on resource [%s]`. 

**Attributo:** `Consumes`

Questo attributo si comporta in maniera analoga a `Produces` ma rispetto all'input di una risorsa. Con i metodi *PUT* e *POST*, per esempio, viene inviato un messaggio al server. Il formato del messaggio è indicato dall'header [Content-Type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type). In questo caso l'header deve combaciare con quanto dichiarato con l'attributo `Consumes`. Una volta fatto questo tramite l'attributo `BodyParam` sarà possibile leggere il messaggio inviato dal client.

```pascal
  [Path('user')]
  TUserResource = class(TObject)
  public
    [POST]
    [Consumes(TMediaType.APPLICATION_JSON)]
    [Produces(TMediaType.APPLICATION_JSON)]
    function AppendUser([BodyParam] AUser: TUser): TUser;

  end;
```

In questo esempio il metodo `AppendUser` si aspetta un messaggio in formato *JSON*. Se il messaggio è effettivamente in questo formato *WiRL* cercherà un **message body reader** in grado di trasformare un *JSON* in *TUser*. Se non lo trova restituirà l'errore `'Unsupported media type [%s] for param [%s]'`.

**Attributo:** `PathParam`

Nel caso sia stato definito un URL tramite l'attributo `Path` contenente un *template*, il contenuto del template verrà associato al parametro decorato con l'attributo `PathParam`.

```pascal
  [Path('user')]
  TUserResource = class(TObject)
  public
    [GET]
    [Produces(TMediaType.APPLICATION_JSON)]
    function GetAllUsers(): TObjectList<TUser>;

    [GET]
    [Path('{id}')]
    [Produces(TMediaType.APPLICATION_JSON)]
    function GetUserById([PathParam('id')] AId: Integer): TUser;

    [GET]
    [Path('{id}/todo/{category}')]
    [Produces(TMediaType.APPLICATION_JSON)]
    function GetUserTodo(
        [PathParam('id')] AId: Integer
        [PathParam('category')] const ACategory: string
    ): TObjectList<TTodo>;
    
```

Prendiamo in considerazione diversi URL:

* `/user`: Questo path farà un modo che venga chiamato il metodo `GetAllUsers`
* `/user/12`: In questo caso verrà chiamato il metodo `GetUserById` e il valore *12*, che è parte del template, verrà passato al parametro `AId`. Infatti il nome del parametro del template `Id` corrisponde al valore dell'attributo `PathParam`.
* `/user/12/todo/done`: in questo caso verrà richiamato il metodo `GetUserTodo` e al parametro `AId` verrà passato *12*, mentre a `ACategory` la stringa *done*.

**Attributo:** `QueryParam`

QueryParam permette di catturare parametri passati tramite [query string](https://en.wikipedia.org/wiki/Query_string).

```pascal
  [Path('user')]
  TUserResource = class(TObject)
  public    
    [GET]
    [Produces(TMediaType.APPLICATION_JSON)]
    function GetUser(
        [QueryParam('name')] const AName: string;
        [QueryParam('email')] const AEmail: string
    ): TObjectList<TUser>;

```

In questo esempio con un URL tipo: `/user?name=lminuti&email=lminuti@examples.com` WiRL imposterà i parametri `AName` e `AEmail` con i valori appropriati.

**Attributo:** `FormParam`

Questo attributo funziona in maniera analoga a `QueryParam` ma cerca in parametri nel corpo del messaggio. `WiRL` in questo caso si aspetta che il corpo del messaggio sia in formato `application/x-www-form-urlencoded`, quindi lo decodifica e si occupa di passare i valori che trova ai parametri del metodo.

**Attributo:** `BodyParam`

Questo attributo permette di leggere l'intero corpo del messaggio a patto che il formato usato per l'invio sia compatibile con quanto indicato nell'attributo `Consumes` ed esista un `message body reader` in grado di trasformare il messaggio nell'oggetto indicato.

```pascal
  [Path('user')]
  TUserResource = class(TObject)
  public
    [POST]
    [Consumes(TMediaType.APPLICATION_JSON)]
    [Produces(TMediaType.APPLICATION_JSON)]
    function AppendUser([BodyParam] AUser: TUser): TUser;

  end;
```

In questo esempio il metodo `AppendUser` si aspetta un messaggio in formato *JSON*. Se il messaggio è effettivamente in questo formato *WiRL* cercherà un **message body reader** in grado di trasformare un *JSON* in *TUser*.

**Altri attributi**

Ci sono altri attributi che in modo simile a quanto visto in precedenza si occupano di leggere: *header*, *cookie*, [token JWT](https://jwt.io/) e altro.

## Conversione dei parametri

In generale quando *WiRL* associa un valore letto dalla richiesta HTTP ad un parametro di un metodo cercherà di convertirlo in maniera appropriata. È possibile personalizzare il modo in cui *WiRL* effettua questo tipo di conversione (es. il formato delle date o il separatore decimale) tramite `IWiRLFormatSetting`.

```pascal
  FServer.AddEngine<TWiRLEngine>('/rest')
    .SetEngineName('RESTEngine')
    .AddApplication('/app')
      .SetResources('*')
      .SetFilters('*')

      .Plugin.Configure<IWiRLFormatSetting>
        .AddFormat(TypeInfo(TDateTime), TWiRLFormatSetting.ISODATE_UTC)
        .BackToApp
    // ...

```

Inoltre se un parametri di un metodo è una classe con un costruttore che prende in input un unico parametro di tipo stringa *WiRL* istanzierà automaticamente l'oggetto passando al costruttore il parametro letto.

```pascal
  TUserIdList = class
  public
    constructor Create(const AList: string);
  end;

  [Path('user')]
  TUserResource = class(TObject)
  public    
    [GET]
    [Produces(TMediaType.APPLICATION_JSON)]
    function GetUsers(
        [QueryParam('id')] AIdList: TUserIdList
    ): TObjectList<TUser>;

```

In questo caso con una richiesta come:

```
GET /user?id=1,2,3,4
```

L'oggetto `AIdList` verrà instanziato passando al costruttore la stringa `'1,2,3,4'`.

## Registrazione

Come ultimo passaggio è necessario registrare la classe che implementa la risorsa all'interno del **registro delle risorse**. Questa operazione di solito viene fatta nella sezione *initialization* della *unit* dove viene definita la classe.

Nel caso della classe `TUserResource` il codice è il seguente:

```pascal
initialization
  TWiRLResourceRegistry.Instance.RegisterResource<TUserResource>;
```

In questo modo *WiRL* riconosce la classe `TUserResource` e, nel momento in cui ne ha bisogno, è in grado di creare un'istanza usando un costruttore senza parametri. Se la classe non dovesse avere un costruttore senza parametri è necessario fornire un metodo anonimo che sia in grado di creare un'istanza dell'oggetto:

```pascal
  TWiRLResourceRegistry.Instance.RegisterResource<TUserResource>(
    function: TObject
    begin
      Result := TUserResource.Create(...);
    end
  );
```

## Conclusioni

In questo articolo abbiamo visto come configurare una classe per processare le chiamate ad una risorsa ReST, come leggere i parametri e come fornire una risposta in output. Nei prossimi articoli vedremo in particolare come pubblicare dati presenti sul database tramite risorse opportune e come personalizzare i message body writer e reader.