WiRL: Risorse e Attributi

Luca Minuti - Feb 19 - - Dev Community

Lo sviluppo di un server ReST con 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}
Enter fullscreen mode Exit fullscreen mode

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:

  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;
Enter fullscreen mode Exit fullscreen mode

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, 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:

  [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;
Enter fullscreen mode Exit fullscreen mode

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.

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

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

Enter fullscreen mode Exit fullscreen mode

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.

Resource schema

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

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

  end;
Enter fullscreen mode Exit fullscreen mode

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.

  [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>;

Enter fullscreen mode Exit fullscreen mode

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.

  [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>;

Enter fullscreen mode Exit fullscreen mode

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.

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

  end;
Enter fullscreen mode Exit fullscreen mode

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

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

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

Enter fullscreen mode Exit fullscreen mode

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.

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

Enter fullscreen mode Exit fullscreen mode

In questo caso con una richiesta come:

GET /user?id=1,2,3,4
Enter fullscreen mode Exit fullscreen mode

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:

initialization
  TWiRLResourceRegistry.Instance.RegisterResource<TUserResource>;
Enter fullscreen mode Exit fullscreen mode

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:

  TWiRLResourceRegistry.Instance.RegisterResource<TUserResource>(
    function: TObject
    begin
      Result := TUserResource.Create(...);
    end
  );
Enter fullscreen mode Exit fullscreen mode

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.

. . . . . . . . . . . .