Présentation
Après avoir fait quelques lignes de commandes, j’essaye de voir ce qui peut-être fait en web avec Rust. J’ai vu qu’il était possible de faire du front via des Web Assemblys mais on va commencer par le Back.
Comme d’hab, plusieurs possibilités …
Axum
Il ne sort pas en premier des listes mais il est issu de l’éco système Tokio que l’on retrouve très vite quand on parle d’asynchrone et de Rust. Axum n’est pas référencé sur leur site. Il parle de Hyper mais en indiquant d’utiliser Warp ou … Axum.
J’ai trouvé une vidéo dont le périmètre proposé me semble intéssant :
axium01.png
Bref … c’est parti
Dépendances
- Tokio : librairie pour l’asynchrone,
- Anyhow : amélioration pour la gestion des erreurs,
- httpc-test : utilitaire de tests réalisé par la personne qui a fait le tuto,
Première route
let routes_hello = Router::new().route(
"/hello",
get(|| async { // On sent que les closures étaient prévues dès le départ
Html("Bonjour le monde !")
}
)
);
Cargo Watch
C’est une option supplémentaire : cargo install cargo-watch
Une commande lisible à souhait :cargo watch -q -c -w src -x run
qui s’explique :
- watch : l’option souhaitée,
- q : mode quiet = le mode watch ne sort rien pour ne pas polluer,
- c : mode clear = entre chaque compile, nettoyage du terminal,
- w : défini le répertoire qui est surveillé par le mode watch,
- x : la commande qui est effectivement lancé.
un autre : cargo watch -q -c -w tests/ -x "test -q quick_dev -- --nocapture"
Middleware, State, Context …
Comme dans d’autres frameworks (express, sveltekit), il est possible de mettre des fonctions (middleware) qui vont permettre de contrôler le flux autour des différentes requêtes.
Par exemple pour l’authentification :
- création d’une fonction qui valide qu’on a bien un cookie :
pub async fn mw_require_auth<B>(cookies: Cookies, req: Request<B>, next: Next<B> ) -> Result<Response> {}
- Un middleware vide ferait juste un
Ok(next.run(req).await)
,
- Un middleware vide ferait juste un
- intégration dans une ou plusieurs routes :
let routes_apis = web::route_tickets::routes(mc.clone()) .route_layer( middleware::from_fn(web::mw_auth::mw_require_auth) );
- Ici, il n’est appliqué qu’à cette route.
Pour mettre en place un contexte, il faut :
- créer une structure (ca va)
- Le passer en paramètre des fonctions nécessaires,
- par contr de par défaut, il se peut qu’Axum l’initialise plusieurs fois ce qui peut poser des soucis de perf ou de … contexte,
- Pour palier cela, il faut … mettre en place un middleware.
Divers
Possibilités de merger des routes,
let routes_hello = Router::new() .merge(routes_hello()) .merge(web::route_login::routes()) .nest("/api", web::route_tickets::routes(mc.clone()))
Les handlers doivent retourner des Router,
Il est possible de retourner des statics qui pointent directement vers un répertoire (utilise une librairie externe)
Les routes prennent en paramètres des extracteurs :
- Query : pour les ?
- Path : pour /…
- Json : …
Les extracteurs doivent être le dernier argument d’un handler,
Est lié à pas mal de composent “tower” ce qui est assez logique car fait partie de la stack tokio,
J’adore :
Arc<Mutex<Vec<Option<Ticket>>>>
(le truc facile à lire plus tard)Un point qui m’a pris un peu de temps : la recherche dans la suppression qui se base sur l’index et pas l’id,
- Dans son tableau, il stocke des Options. Quand il supprime, il vide pas le table, il remplace la valeur (Some) par None,
- Cela m’a également permis de comprendre pourquoi il faisait un filtre sur la liste : filter_map exclue directement les Nones (malin !)
Une syntaxe que je n’avais pas encore vu. Permet d’implémenter un trait généric en indiquant ce que S doit à minima impléme,ter
impl<S: Send + Sync> FromRequestParts<S> for Ctx { }
Routes supplémentaires
Juste pour refaire deux actions simples : ajout d’une route pour récupérer un élement et une route pour mettre à jour.
Pour récupérer :
- Ajout d’une fonction sur le controleur model pour récupérer la valeur,
- déjà la pas simple pour sortir un élement de la liste …,
pub async fn get_ticket(&self, ctx: Ctx, id: u64) -> Result<Ticket> { let mut store = self.tickets_store.lock().unwrap(); let ticket = store .get(id as usize) .and_then(|t| t.clone()); // Est-ce que clone est une bonne idée ou la solution ? ticket.ok_or(Error::TicketDeleteFailNotFound { id }) }
- déjà la pas simple pour sortir un élement de la liste …,
- Ajout d’une fonction dans route_tickets qui utilise sur cette nouvelle méthode,
- Utilisation des différents extractors” : State, contexte et Path,
- Enregistrement de la route dans la liste des routes,
- Ici, une simple chaîne supplémentaire dans l’existant :
"/tickets/:id", delete(delete_ticket).get(get_ticket)
,
- Ici, une simple chaîne supplémentaire dans l’existant :
- Ajout d’un test dans le fichier quick_dev pour valider :
hc.do_get("/api/tickets/1").await?.print().await?;
, - Et voilà :)
Pour mettre à jour :
- Ajout d’un model spécifique pour
TicketForUpdate
,- Possible de réutiliser celui de création mais c’est pour refaire la manip,
- Ajout de la méthode au niveau du controlleur,
- Enregistrement de la route,
- Note : il semble qu’il y ait un ordre dans les paramètres des extractors : Path avant JSON
- Ajout d’un test
Bilan
Le point fort du framework c’est les extracteurs et le découpage entre route, layer & contexte. Via les premiers, il est possible de demander les éléments nécessaires pour faire un traitement. En cas, de besoin il est assez simple de faire son propre extractor pour créer un contexte.
Par contre, le code est parfois complexe pour pas grand chose mais là… est-ce que cela vient de Rust ou d’Axum …