Sinora, parlando di Rust abbiamo visto il lato client della Rete. È giusto ora vedere quello server che al giorno d'oggi può portare grandi risultati permettendo di implementare API REST, servizi web in genere e tanti microservizi per strutturare le nostre architetture. Analizziamo un framework di nome warp che si sposa perfettamente con la programmazione asincrona di cui abbiamo già parlato. Scopriremo, in questa lezione e nelle successive, che questo framework modulare è ricco di componenti che contemplano server, router, filtri e qualsiasi altro meccanismo per poter rispondere alle richieste dei client. Pertanto potrà essere impiegato a partire dagli usi minimali fino a scenari estremamente articolati.
Preparazione del progetto Rust
Approcciamo direttamente lo sviluppo di una API, non molto estesa ma che sarà in grado di mettere in luce i principali meccanismi di warp in attesa di approfondirla al meglio. Predisponiamo un progetto cargo
di nome esempio-api
con:
$ cargo new esempio-api
e aggiungiamo entrambe le dipendenze di cui abbiamo bisogno:
$ cargo add warp
$ cargo add tokio
A questo punto facciamo partire il nostro primo server con il seguente codice:
use warp::Filter;
#[tokio::main]
async fn main() {
let raddoppia = warp::path!("raddoppia" / i16)
.map(|valore| format!("Il doppio di {} è {}", valore, valore*2));
warp::serve(raddoppia)
.run(([127, 0, 0, 1], 8080))
.await;
}
Il codice crea un servizio, di un solo metodo, accessibile sulla porta 8080
in locale (indirizzo localhost
) che se invocato nel percorso /raddoppia
seguito da un numero intero restituisce un messaggio in cui si esplicita il valore doppio del numero passato. Se ad esempio viene invocato http://localhost:8080/raddoppia/5
viene restituito "Il doppio di 5 è 10."
Il codice Rust è diviso in due parti racchiuse in uno stesso main
:
- la prima definisce un metodo della API (l'unico al momento) che si forma con
warp::path
. In tal caso si indica che il path inizierà con/raddoppia
e proseguirà con un numero intero a 16 bit. Ilmap
introduce il codice costituito dalla closure che raddoppia un valore passato e lo immette in un messaggio; - il secondo pezzo lancia il server (
warp::serve(raddoppia)
) indicando indirizzo e porta di ascolto e stabilendo che l'unico codice implementato sarà proprioraddoppia
.
Per provare il codice possiamo agire da browser o con altri strumenti di test HTTP come il tradizionale curl
attivato con il comando:
curl localhost:8080/raddoppia/23
restituisce
Il doppio di 23 è 46
Il routing
E se volessimo aggiungere un altro tipo di risposta da invocare su un indirizzo diverso ma senza compromettere raddoppia
? Dovremmo implementare un po' di routing tra le richieste in modo che, al riconoscimento di un percorso, Warp saprebbe quale funzionalità applicare. Supponiamo di aggiungere anche il percorso /ciao
che, se invocato seguito da una stringa che rappresenta un nome, viene restituito un messaggio di saluto all'indirizzo di questo nome:
use warp::Filter;
#[tokio::main]
async fn main() {
let raddoppia = warp::path!("raddoppia" / i16)
.map(|valore| format!("Il doppio di {} è {}", valore, valore*2));
let ciao = warp::path!("ciao" / String)
.map(|nome| format!("Ciao {}", nome));
let router = raddoppia.or(ciao);
warp::serve(router)
.run(([127, 0, 0, 1], 8080))
.await;
}
Notiamo che raddoppia
è rimasta identica ma abbiamo aggiunto il metodo ciao
. Quest'ultimo è simile al precedente solo che consiste in un semplice esercizio di saluto.
La cosa interessante è che abbiamo creato il router
, componente che con or
scandisce che la risposta sarà gestita o da una funzione o dall'altra a seconda del formato della richiesta. In qualsiasi altro caso non verrà fornita risposta. Notiamo inoltre che il router
è stato passato a warp::serve
che così sarà a conoscenza di entrambi gli indirizzi.
Pertanto potremo provare le seguenti risposte:
$ curl localhost:8080/raddoppia/23
Il doppio di 23 è 46
$ curl localhost:8080/ciao/Simone
Ciao Simone
Un aspetto che scopriamo di Warp è che dispone in questo caso di molte funzioni per la concatenazione di funzionalità e questo rende il codice molto flessibile ma anche comodo da interpretare.
Questo meccanismo permette di definire anche i metodi stessi. Vediamo questa rappresentazione di un metodo HTTP/POST (versione esemplificativa da completare):
warp::post()
.and(warp::path("v1"))
.and(warp::path("prodotti"))
.and(warp::path::end())
.and(...)
.and(...);
Vediamo che si usa il metodo post
e si va poi a definire con un elenco di and
i vari blocchi del percorso ed in fondo c'è spazio per varie funzioni di conversione ed elaborazione.
Tutto ciò verrà approfondito nei prossimi esempi della guida Rust.