Rust, Rest = Axum (1/?)

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),
  • 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 })
      }
  • 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),
  • 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 …