Blog IA/ML Programmation en Rust à l’aide de l'IA : tutoriel
Mise à jour : January 27, 2025
Lecture : 31 min

Programmation en Rust à l’aide de l'IA : tutoriel

Poursuivez votre apprentissage de la programmation en Rust à l'aide de ce tutoriel et des suggestions de code alimentées par l'IA de GitLab Duo.

codewithheart.png

Il y a 20 ans, pour apprendre un nouveau langage de programmation, il fallait installer la bibliothèque MSDN de Visual Studio 6 avec 6 CD-ROM. Trouver les bons algorithmes prenait du temps et nécessitait des livres et des recherches manuelles. Aujourd'hui, grâce à la collaboration à distance et à l'intelligence artificielle (IA), vous pouvez facilement créer un espace de développement à distance, partager votre écran et coder en groupe. De plus, GitLab Duo offre des suggestions de code alimentées par l'IA, adaptées à votre style de programmation et à votre expérience, même avec peu d'informations et de contexte.

Ce tutoriel basé sur notre article d'introduction à Rust vous guidera dans le développement d'une application simple de lecteur de flux.

Préparations

Configurez VS Code et votre environnement de développement Rust.

Suggestions de code

Vérifiez les suggestions de code avant de les accepter. GitLab Duo génère des suggestions de code en temps réel que vous pouvez accepter en appuyant sur la touche tab. Notez qu'il est plus fiable d'écrire du nouveau code que de refactoriser un code existant. Notez aussi que l'IA peut proposer des suggestions différentes pour le même code. Tenez compte des limitations connues qui peuvent affecter votre apprentissage.

Astuce : les suggestions de code prennent en charge les instructions multilignes. Affinez les spécifications si besoin pour obtenir de meilleures suggestions.

    // Create a function that iterates over the source array
    // and fetches the data using HTTP from the RSS feed items.
    // Store the results in a new hash map.
    // Print the hash map to the terminal.

Le volet de l'extension VS Code s'affiche lorsqu'une suggestion est proposée. Utilisez tab pour accepter les lignes suggérées, ou cmd cursor right pour accepter un mot. Le menu à trois points vous permet d'afficher la barre d'outils à tout moment.

Volet de l'extension VS Code des suggestions de code de GitLab Duo avec des instructions

Ressources Rust

Rust est l'un des langages pris en charge par les suggestions de code. Le tutoriel « Rust by Example » et le livre officiel « Le langage de programmation Rust » sont une bonne aide pour les débutants. Nous les utiliserons également dans cet article.

Création d'une application de lecture de flux

Il existe plusieurs façons d'apprendre Rust en créant une application. Vous pouvez utiliser des bibliothèques Rust, appelées Crates. Nous les explorerons pus tard dans ce tutoriel. Vous pouvez aussi créer une application pour traiter des fichiers image en ligne de commande, résoudre un labyrinthe ou des Sudokus, ou même développer des jeux. Le livre Hands-on Rust apprend Rust par la création d'un jeu d'exploration de donjon. Ma collègue, Fatima Sarah Khalid, a conçu Dragon Realm en C++ en s'aidant de l'IA.

Nous allons construire une application pour collecter des informations depuis des versions de sécurité, des articles de blog et des forums comme Hacker News, via des flux RSS. Nous filtrerons certains mots-clés ou des versions spécifiques dans ces flux. Voici ce que notre application devra faire :

  1. Récupérer des données depuis des sites web HTTP, API REST, mais surtout depuis des flux RSS.
  2. Analyser les données.
  3. Présenter les données à l'utilisateur ou les sauvegarder.
  4. Optimiser la lecture des flux.

Les données fournies par l'application dans notre exemple sont présentées dans cet article, après les étapes d'apprentissage :

Terminal VS Code, exécution de Cargo avec des sorties d'éléments formatées

L'application doit être modulaire, servant de base pour ajouter d'autres données, filtres et hooks pour des actions ultérieures.

Initialisation du projet

Rappel : cargo init dans la racine du projet crée la structure du fichier, y compris le point d'entrée main(). Nous allons créer et utiliser des modules Rust à l'étape suivante.

D'abord, créez un répertoire nommé learn-rust-ai-app-reader, ouvrez-le et exécutez cargo init, ce qui initialise également (git init) un dépôt Git local. Configurez ensuite le dépôt distant avec l'URL par exemple https://gitlab.com/gitlab-de/use-cases/ai/learn-with-ai/learn-rust-ai-app-reader, ajustez le chemin de votre espace de nommage, et effectuez un push pour créer automatiquement un nouveau projet privé sur GitLab.

mkdir learn-rust-ai-app-reader
cd learn-rust-ai-app-reader

cargo init

git remote add origin https://gitlab.com/gitlab-de/use-cases/ai/learn-with-ai/learn-rust-ai-app-reader.git
git push --set-upstream origin main

Ouvrez VS Code à partir du répertoire créé. La CLI code ouvre une fenêtre VS Code sur macOS.

code .

Définition des URL des flux RSS

Ajoutez une table de hachage pour stocker les URL des flux RSS dans le fichier src/main.rs dans la fonction main(). Utilisez les suggestions de code de GitLab Duo pour créer un objet HashMap avec un commentaire multilignes et initialisez-le avec les valeurs par défaut pour Hacker News et TechCrunch. Vérifiez l'exactitude des URL incluses dans les suggestions.

fn main() {
    // Define RSS feed URLs in the variable rss_feeds
    // Use a HashMap
    // Add Hacker News and TechCrunch
    // Ensure to use String as type

}

Le commentaire intégré au code doit inclure :

  1. Le nom de la variable rss_feeds.
  2. Le type HashMap.
  3. Les paires clé/valeur initiales.
  4. La string comme type utilisable avec les appels to_string()).

Voici une suggestion possible :

use std::collections::HashMap;

fn main() {##$_0A$##    // Define RSS feed URLs in the variable rss_feeds##$_0A$##    // Use a HashMap##$_0A$##    // Add Hacker News and TechCrunch##$_0A$##    // Ensure to use String as type##$_0A$##    let rss_feeds = HashMap::from([##$_0A$##        ("Hacker News".to_string(), "https://news.ycombinator.com/rss".to_string()),##$_0A$##        ("TechCrunch".to_string(), "https://techcrunch.com/feed/".to_string()),##$_0A$##    ]);##$_0A$####$_0A$##}

VS Code avec les suggestions de code pour les URL des flux RSS pour Hacker News et TechCrunch

Ouvrez un terminal dans VS Code (cmd+maj+p, recherchez terminal), exécutez cargo build pour appliquer les modifications. Ajoutez la ligne d'importation use std::collections::HashMap; suite au message d'erreur.

Ensuite, utilisez les URL des flux RSS. Dans notre article précédent, nous avons expliqué la division du code en fonctions. Ce tutoriel se concentre sur l'organisation modulaire du code avec des modules Rust.

Modules

Les modules organisent le code et peuvent restreindre l'accès aux fonctions à partir de la portée main(). Pour notre application, nous récupérerons le flux RSS et analyserons la réponse XML. La structure appelante main() n'accèdera qu'à la fonction get_feeds(). Les autres fonctionnalités sont disponibles dans le module.

Créez un fichier feed_reader.rs dans le répertoire src/. Demandez aux suggestions de code de créer un module public nommé feed_reader avec une fonction publique get_feeds() prenant une string HashMap en entrée. Assurez-vous que le nom du fichier et du module correspondent et suivent la structure de module Rust.

Suggestions de code : créez un module public, avec des types de fonctions et d'intrants

En fournissant aux suggestions de code le nom de la variable et son type, le module std::collections::HashMap sera automatiquement importé. Astuce : ajustez les commentaires et les types de variables pour améliorer les suggestions. Passer des paramètres de fonction sous forme de références d'objet est une bonne pratique en Rust.

// Create public module feed_reader
// Define get_feeds() function which takes rss_feeds as String HashMap reference as input
pub mod feed_reader {
    use std::collections::HashMap;

    pub fn get_feeds(rss_feeds: &HashMap<String, String>) {
        // Do something with the RSS feeds
    }
}

Suggestions de code : module public avec la fonction  et la variable d'intrant suggérée

Pour guider les suggestions de code, suivez ces étapes :

  1. // Iterate over the RSS feed URLs
  2. // Fetch URL content
  3. // Parse XML body
  4. // Print the result

Suggestions de code : module public avec la fonction , étape 1 : itération

Suggestions de code : module public avec la fonction , étape 2 : récupération du contenu de l'URL

Suggestions de code : module public avec la fonction , étape 3 : analyse du corps XML

Suggestions de code : module public avec la fonction , étape 4 : impression des résultats

Suggestion de code possible :

// Create public module feed_reader
// Define get_feeds() function which takes rss_feeds as String HashMap reference as input
pub mod feed_reader {
    use std::collections::HashMap;

    pub fn get_feeds(rss_feeds: &HashMap<String, String>) {
        // Iterate over the RSS feed URLs
        for (name, url) in rss_feeds {
            println!("{}: {}", name, url);

            // Fetch URL content
            let body = reqwest::blocking::get(url).unwrap().text().unwrap();

            // Parse XML body
            let parsed_body = roxmltree::Document::parse(&body).unwrap();

            // Print the result
            println!("{:#?}", parsed_body);
        }
    }
}

Remarquez le nouveau mot-clé : unwrap(). Rust n'accepte pas les valeurs null et utilise le type Option pour représenter les valeurs. Si vous avez la certitude d'utiliser un type encapsulé comme Text ou String, vous pouvez appeler la méthode unwrap() pour obtenir la valeur, mais elle provoquera un arrêt brutal du programme si la valeur est None.

Remarque : les suggestions de code se réfèrent à la fonction reqwest::blocking::get pour suivre l'instruction // Fetch URL content du commentaire. Le nom de la crate reqwest n'est pas une faute de frappe. Cette crate simplifie les requêtes HTTP, en mode asynchrone ou bloquant.

L'analyse XML peut varier selon les flux RSS, alors testez la fonction get_feeds() et ajustez le code si nécessaire.

Appel de la fonction du module dans main()

La fonction main() ne connaît pas encore la fonction get_feeds(). Nous devons donc importer son module. Contrairement à d'autres langages de programmation, qui utilisent les mots-clés include ou import, Rust utilise des répertoires de fichiers

pour organiser les modules. Dans notre exemple, les deux fichiers source existent au même niveau de répertoire. feed_reader.rs est interprété comme une crate, contenant un module appelé feed_reader, qui définit la fonction get_feeds().

src/
  main.rs
  feed_reader.rs

Pour accéder à get_feeds() à partir du fichier feed_reader.rs, commencez par indiquer le chemin du module dans la portée main.rs, puis appeler le chemin complet de la fonction.

mod feed_reader;

fn main() {

    feed_reader::feed_reader::get_feeds(&rss_feeds);

Utilisez le mot-clé use pour importer le chemin complet de la fonction avec un nom court.

mod feed_reader;
use feed_reader::feed_reader::get_feeds;

fn main() {

    get_feeds(&rss_feeds);

Astuce : consultez l'article « Clear explanation of the Rust module system » pour plus de détails.


fn main() {
    // ...

    // Print feed_reader get_feeds() output
    println!("{}", feed_reader::get_feeds(&rss_feeds));
use std::collections::HashMap;

mod feed_reader;
// Alternative: Import full function path
//use feed_reader::feed_reader::get_feeds;

fn main() {
    // Define RSS feed URLs in the variable rss_feeds
    // Use a HashMap
    // Add Hacker News and TechCrunch
    // Ensure to use String as type
    let rss_feeds = HashMap::from([
        ("Hacker News".to_string(), "https://news.ycombinator.com/rss".to_string()),
        ("TechCrunch".to_string(), "https://techcrunch.com/feed/".to_string()),
    ]);

    // Call get_feeds() from feed_reader module
    feed_reader::feed_reader::get_feeds(&rss_feeds);
    // Alternative: Imported full path, use short path here.
    //get_feeds(&rss_feeds);
}

Exécutez cargo build à nouveau dans le terminal pour compiler le code.

cargo build

Demander des suggestions de code pour des bibliothèques courantes (comme les requêtes HTTP ou l'analyse XML) peut provoquer des erreurs de compilation telles que :

  1. Erreur : could not find blocking in reqwest. Solution : activez la fonctionnalité blocking pour la crate dans Config.toml: reqwest = { version = "0.11.20", features = ["blocking"] }.
  2. Erreur : failed to resolve: use of undeclared crate or module reqwest. Solution : ajoutez la crate reqwest.
  3. Erreur : failed to resolve: use of undeclared crate or module roxmltree. Solution : ajoutez la crate roxmltree.
vim Config.toml

reqwest = { version = "0.11.20", features = ["blocking"] }
cargo add reqwest
cargo add roxmltree

Astuce : copiez le message d'erreur avec l'en-tête Rust <error message> dans un navigateur pour vérifier si la crate manquante est disponible. Cette recherche vous dirigera sur crates.io. Ajoutez ensuite les dépendances manquantes.

Après la compilation, exécutez le code avec cargo run et vérifiez la sortie du flux RSS de Hacker News.

Terminal VS Code, exécution de Cargo pour récupérer le flux XML de Hacker News

Prochaine étape : analyse XML dans un format lisible par un humain Nous allons maintenant explorer les solutions existantes et le rôle des crates Rust.

Crates

Les flux RSS suivent des protocoles et spécifications communs. Pour analyser les éléments XML et comprendre la structure de l'objet sous-jacent, vérifiez si un développeur a déjà résolu ce problème et utilisez son code.

En Rust, le code réutilisable est organisé en Crates et disponible sur crates.io. Ajoutez ces dépendances dans le fichier Config.toml dans la section [dependencies] ou avec cargo add <name>.

Pour notre application, utilisez la crate feed-rs en exécutant la commande suivante dans le terminal :

cargo add feed-rs

Terminal VS Code Terminal : ajout d'une crate, vérification dans le fichier Config.toml

feed-rs pour analyser un flux XML

Accédez à src/feed_reader.rs pour modifier l'analyse XML. GitLab Duo sait appeler la fonction parser::parse de la crate feed-rs. Les suggestion de code comprennent le cas particulier : feed-rs attend des octets bruts en entrée, et non des chaînes pour déterminer l'encodage. Fournissez des instructions dans le commentaire pour obtenir le résultat prévu.

            // Parse XML body with feed_rs parser, input in bytes
            let parsed_body = feed_rs::parser::parse(body.as_bytes()).unwrap();

Suggestions de code : module public avec la fonction , étape 5 : modification de l'analyseur XML en feed-rs

Les avantages de feed-rs se révèlent en exécutant votre programme avec la commande cargo run : les clés et valeurs sont converties en types d'objets Rust réutilisables.

Terminal VS Code, exécution de Cargo pour récupérer le flux XML de Hacker News

Configuration du runtime : arguments de programme

Jusqu'à présent, nous avons utilisé des valeurs de flux RSS codées en dur. La prochaine étape est de configurer ces flux RSS au moment de l'exécution.

Rust propose des arguments de programme via la bibliothèque « Std misc » standard. Il est plus rapide d'apprendre l'analyse des arguments directement que d'utiliser des crates (par exemple, la crate clap) ou de passer par un fichier de configuration ou un autre format (TOML, YAML). Pour cet article, j'ai bien essayé d'autres pistes, mais sans succès. Relevez le défi et essayez de configurer les flux RSS d'une autre manière.

Une méthode simple consiste à passer les paramètres sous la forme "name,url" avec les valeurs séparées par une virgule , pour extraire le nom et l'URL. Demandez aux suggestions de code d'effectuer cette action et d'inclure les nouvelles valeurs dans la table de hachage rss_feeds. La variable peut ne pas être modifiable et doit être remplacée par let mut rss_feeds.

Accédez à src/main.rs et ajoutez le code suivant à la fonction main() après la variable rss_feeds. Écrivez un commentaire qui définit les arguments de programme et vérifiez le code suggéré.

    // Program args, format "name,url"
    // Split value by , into name, url and add to rss_feeds

Suggestions de code pour les arguments de programme et découpage des paires nom,URL pour la variable rss_feeds

Dans notre exemple, le code est le suivant :

fn main() {
    // Define RSS feed URLs in the variable rss_feeds
    // Use a HashMap
    // Add Hacker News and TechCrunch
    // Ensure to use String as type
    let mut rss_feeds = HashMap::from([
        ("Hacker News".to_string(), "https://news.ycombinator.com/rss".to_string()),
        ("TechCrunch".to_string(), "https://techcrunch.com/feed/".to_string()),
    ]);

    // Program args, format "name,url"
    // Split value by , into name, url and add to rss_feeds
    for arg in std::env::args().skip(1) {
        let mut split = arg.split(",");
        let name = split.next().unwrap();
        let url = split.next().unwrap();
        rss_feeds.insert(name.to_string(), url.to_string());
    }

    // Call get_feeds() from feed_reader module
    feed_reader::feed_reader::get_feeds(&rss_feeds);
    // Alternative: Imported full path, use short path here.
    //get_feeds(&rss_feeds);
}

Passez les arguments directement avec cargo run, avant les arguments contenant --. Ajoutez des guillemets doubles à tous les arguments et une virgule après le nom. Utilisez l'URL du flux RSS comme argument. Séparez les arguments par des espaces.

cargo build

cargo run -- "GitLab Blog,https://about.gitlab.com/atom.xml" "CNCF,https://www.cncf.io/feed/"

Terminal VS Code, exemple de sortie de flux RSS pour le blog GitLab

Gestion des erreurs des intrants saisis

Si l'entrée ne correspond pas aux attentes du programme, générez une erreur pour aider à corriger les arguments, par exemple en cas d'URL invalide. Ajoutez un commentaire dans le code pour que les suggestions de code incluent une vérification de l'URL.

    // Ensure that URL contains a valid format, otherwise throw an error

Vérifiez si url commence par http:// ou https://. Sinon, utilisez la macro panic! pour générer une erreur. Dans notre exemple, le code est le suivant :

    // Program args, format "name,url"
    // Split value by , into name, url and add to rss_feeds
    for arg in std::env::args().skip(1) {
        let mut split = arg.split(",");
        let name = split.next().unwrap();
        let url = split.next().unwrap();

        // Ensure that URL contains a valid format, otherwise throw an error
        if !url.starts_with("http://") && !url.starts_with("https://") {
            panic!("Invalid URL format: {}", url);
        }

        rss_feeds.insert(name.to_string(), url.to_string());
    }

Testez la gestion des erreurs en supprimant : dans l'une des chaînes d'URL. Ajoutez la variable d'environnement RUST_BACKTRACE=full pour un affichage plus détaillé des résultats lorsque l'appel panic().

RUST_BACKTRACE=full cargo run -- "GitLab Blog,https://about.gitlab.com/atom.xml" "CNCF,https//www.cncf.io/feed/"

Terminal VS Code avec un format d'URL incorrect, traçage des erreurs suite à un arrêt brutal du programme

Persistance et stockage des données

Une solution classique consiste à stocker les données du flux analysé dans un nouveau fichier. Demandez aux suggestions de code de générer un nom de fichier incluant le nom du flux RSS et la date au format ISO.

    // Parse XML body with feed_rs parser, input in bytes
    let parsed_body = feed_rs::parser::parse(body.as_bytes()).unwrap();

    // Print the result
    println!("{:#?}", parsed_body);

    // Dump the parsed body to a file, as name-current-iso-date.xml
    let now = chrono::offset::Local::now();
    let filename = format!("{}-{}.xml", name, now.format("%Y-%m-%d"));
    let mut file = std::fs::File::create(filename).unwrap();
    file.write_all(body.as_bytes()).unwrap();

Utilisez la crate chrono. Ajoutez-la à l'aide de cargo add chrono, puis exécutez cargo build et cargo run.

Les fichiers seront enregistrés dans le répertoire où cargo run est exécuté. Si vous exécutez le binaire directement dans le répertoire target/debug/, les fichiers y seront sauvegardés.

VS Code avec fichier de contenu du flux RSS CNCF, enregistré sur le disque

Optimisation

Les URL dans rss_feeds sont traitées de manière séquentielle, ce qui peut être lent avec plus de 100 URL. Pourquoi ne pas les traiter en parallèle ?

Exécution asynchrone

Rust permet l'exécution asynchrone avec des threads.

La méthode la plus simple est de créer un thread pour chaque URL de flux RSS. Nous explorerons les stratégies d'optimisation ultérieurement. Avant de passer à l'exécution parallèle, mesurez le temps d'exécution séquentiel en faisant précéder time par cargo run.

time cargo run -- "GitLab Blog,https://about.gitlab.com/atom.xml" "CNCF,https://www.cncf.io/feed/"

0.21s user 0.08s system 10% cpu 2.898 total

Cet exercice peut nécessiter du codage manuel. Conservez l'état séquentiel dans une validation Git et une nouvelle branche sequential-exec pour comparer l'impact de l'exécution parallèle.

git commit -avm "Sequential execution working"
git checkout -b sequential-exec
git push -u origin sequential-exec

git checkout main

Création de threads

Ouvrez src/feed_reader.rs et réusinez la fonction get_feeds(). Lancez une validation Git de l'état actuel, puis supprimez le contenu de la portée de la fonction. Ajoutez les commentaires suivants dans le code comme instructions pour les suggestions de code :

  1. // Store threads in vector : stockez les gestionnaires de thread dans un vecteur pour attendre que tous les threads se terminent à la fin de l'appel de la fonction.
  2. // Loop over rss_feeds and spawn threads : créez un code standard pour itérer sur les flux RSS et générer un thread pour chacun.

Ajoutez les instructions use suivantes pour les modules thread et time.

    use std::thread;
    use std::time::Duration;

Continuez à écrire le code et fermez la boucle for. Ajoutez les gestionnaires de thread au vecteur threads et joignez-les à la fin de la fonction.

    pub fn get_feeds(rss_feeds: &HashMap<String, String>) {

        // Store threads in vector
        let mut threads: Vec<thread::JoinHandle<()>> = Vec::new();

        // Loop over rss_feeds and spawn threads
        for (name, url) in rss_feeds {
            let thread_name = name.clone();
            let thread_url = url.clone();
            let thread = thread::spawn(move || {

            });
            threads.push(thread);
        }

        // Join threads
        for thread in threads {
            thread.join().unwrap();
        }
    }

Ajoutez la crate thread, compilez et exécutez le code.

cargo add thread

cargo build

cargo run -- "GitLab Blog,https://about.gitlab.com/atom.xml" "CNCF,https://www.cncf.io/feed/"

Aucune donnée n'est traitée ni affichée. Explorez les nouveaux mots-clés avant de réintégrer la fonctionnalité.

Portées, threads et fermetures des fonctions

Le code suggéré utilise de nouveaux mots-clés et design patterns. Le gestionnaire de thread de type thread::JoinHandle permet d'attendre la fin des threads (join()).

thread::spawn() génère un thread pour passer un objet fonction. Une expression de fermeture est passée en tant que fonction anonyme. Les paramètres de fermeture sont passés avec la syntaxe ||. La fermeture move déplace les variables de portée de la fonction dans celle du thread. Pas besoin de préciser manuellement les variables qui doivent être passées dans la nouvelle portée de la fonction/fermeture.

Cependant, rss_feeds est passé comme référence & par la structure d'appel de la fonction get_feeds() et n'est valide que dans la portée de la fonction. Utilisez le code suivant pour provoquer une erreur :

pub fn get_feeds(rss_feeds: &HashMap<String, String>) {

    // Store threads in vector
    let mut threads: Vec<thread::JoinHandle<()>> = Vec::new();

    // Loop over rss_feeds and spawn threads
    for (key, value) in rss_feeds {
        let thread = thread::spawn(move || {
            println!("{}", key);
        });
    }
}

Terminal VS Code, erreur liée à la portée de la variable et aux références avec une fermeture déplacée dans un thread

La variable key, pourtant créée dans la portée de la fonction, fait référence à rss_feeds et ne peut pas être déplacée dans la portée du thread. Les valeurs accessibles depuis la table de hachage des paramètres de fonction rss_feeds nécessitent une copie locale avec clone().

Terminal VS Code, création de thread avec clone

pub fn get_feeds(rss_feeds: &HashMap<String, String>) {

    // Store threads in vector
    let mut threads: Vec<thread::JoinHandle<()>> = Vec::new();

    // Loop over rss_feeds and spawn threads
    for (name, url) in rss_feeds {
        let thread_name = name.clone();
        let thread_url = url.clone();
        let thread = thread::spawn(move || {
            // Use thread_name and thread_url as values, see next chapter for instructions.

Analyse du flux XML dans les types d'objets

L'étape suivante consiste à analyser le flux RSS dans la fermeture du thread. Ajoutez les commentaires suivants dans le code pour guider les suggestions de code :

  1. // Parse XML body with feed_rs parser, input in bytes : récupérer le contenu de l'URL du flux RSS et l'analyser avec les fonctions de la crate feed_rs.
  2. // Check feed_type attribute feed_rs::model::FeedType::RSS2 or Atom and print its name : extraire le type de flux pour comparer l'attribut feed_type avec feed_rs::model::FeedType. Les suggestions de code doivent aussi recevoir des instructions avec les valeurs enum exactes à comparer.

Demande adressée aux suggestions de code de comparer les types de flux spécifiques

            // Parse XML body with feed_rs parser, input in bytes
            let body = reqwest::blocking::get(thread_url).unwrap().bytes().unwrap();
            let feed = feed_rs::parser::parse(body.as_ref()).unwrap();

            // Check feed_type attribute feed_rs::model::FeedType::RSS2 or Atom and print its name
            if feed.feed_type == feed_rs::model::FeedType::RSS2 {
                println!("{} is an RSS2 feed", thread_name);
            } else if feed.feed_type == feed_rs::model::FeedType::Atom {
                println!("{} is an Atom feed", thread_name);
            }

Exécutez le programme et vérifiez les données de sortie.

time cargo run -- "GitLab Blog,https://about.gitlab.com/atom.xml" "CNCF,https://www.cncf.io/feed/"

CNCF is an RSS2 feed
TechCrunch is an RSS2 feed
GitLab Blog is an Atom feed
Hacker News is an RSS2 feed

Pour cela, ouvrez les URL des flux dans le navigateur ou en inspectant les fichiers téléchargés.

Hacker News utilise RSS 2.0, avec channel(title,link,description,item(title,link,pubDate,comments)). TechCrunch et le blog de la CNCF ont une structure similaire.

<rss version="2.0"><channel><title>Hacker News</title><link>https://news.ycombinator.com/</link><description>Links for the intellectually curious, ranked by readers.</description><item><title>Writing a debugger from scratch: Breakpoints</title><link>https://www.timdbg.com/posts/writing-a-debugger-from-scratch-part-5/</link><pubDate>Wed, 27 Sep 2023 06:31:25 +0000</pubDate><comments>https://news.ycombinator.com/item?id=37670938</comments><description><![CDATA[<a href="https://news.ycombinator.com/item?id=37670938">Comments</a>]]></description></item><item>

Le blog de GitLab utilise le format de flux Atom, similaire à RSS, mais nécessitant une logique d'analyse différente.

<?xml version='1.0' encoding='utf-8' ?>
<feed xmlns='http://www.w3.org/2005/Atom'>
<!-- / Get release posts -->
<!-- / Get blog posts -->
<title>GitLab</title>
<id>https://about.gitlab.com/blog</id>
<link href='https://about.gitlab.com/blog/' />
<updated>2023-09-26T00:00:00+00:00</updated>
<author>
<name>The GitLab Team</name>
</author>
<entry>
<title>Atlassian Server ending: Goodbye disjointed toolchain, hello DevSecOps platform</title>
<link href='https://about.gitlab.com/blog/2023/09/26/atlassian-server-ending-move-to-a-single-devsecops-platform/' rel='alternate' />
<id>https://about.gitlab.com/blog/2023/09/26/atlassian-server-ending-move-to-a-single-devsecops-platform/</id>
<published>2023-09-26T00:00:00+00:00</published>
<updated>2023-09-26T00:00:00+00:00</updated>
<author>
<name>Dave Steer, Justin Farris</name>
</author>

Cartographie des types de données de flux génériques

Pour utiliser la fonction roxmltree::Document::parse vous devez comprendre l'arborescence des nœuds XML et ses noms de balise. Heureusement, feed_rs::model::Feed gère à la fois les flux RSS et Atom. Nous pouvons donc continuer avec la crate feed_rs.

  1. Atom : Feed->Feed, Entry->Entry
  2. RSS : Channel->Feed, Item->Entry

Nous devons extraire les attributs requis et associer leurs types de données. Consultez la documentation feed_rs::model pour comprendre les structures, leurs champs et implémentations. Cela évitera les erreurs de conversion et les échecs de compilation, spécifiques à l'implémentation de feed_rs.

La structure Feed fournit un title de type Option<Text> (avec une valeur ou vide). La structure Entry fournit les éléments suivants :

  1. title : Option<Text> avec Text et le champ content comme la String.
  2. updated : Option<DateTime<Utc>> avec DateTime avec la méthode format().
  3. summary : Option<Text> Text et le champ content comme la String.
  4. links : Vec<Link>, vecteur avec les éléments Link. L'attribut href fournit la chaîne d'URL brute.

Utilisez ces connaissances pour extraire les données des intrants de flux. Appelez unwrap() sur tous les types Option et ajoutez des instructions explicites aux suggestions de code.

                // https://docs.rs/feed-rs/latest/feed_rs/model/struct.Feed.html
                // https://docs.rs/feed-rs/latest/feed_rs/model/struct.Entry.html
                // Loop over all entries, and print
                // title.unwrap().content
                // published.unwrap().format
                // summary.unwrap().content
                // links href as joined string
                for entry in feed.entries {
                    println!("Title: {}", entry.title.unwrap().content);
                    println!("Published: {}", entry.published.unwrap().format("%Y-%m-%d %H:%M:%S"));
                    println!("Summary: {}", entry.summary.unwrap().content);
                    println!("Links: {:?}", entry.links.iter().map(|link| link.href.clone()).collect::<Vec<String>>().join(", "));
                    println!();
                }

Suggestions de code pour afficher les types d'éléments de flux, avec des exigences spécifiques

Gestion des erreurs avec l'Option unwrap()

Compilez, exécutez le programme, puis continuez avec des instructions multilignes. Remarque : unwrap() appelle la macro panic! et bloque le programme si des valeurs sont vides, comme un champ summary non défini dans les données du flux.

GitLab Blog is an Atom feed
Title: How the Colmena project uses GitLab to support citizen journalists
Published: 2023-09-27 00:00:00
thread '<unnamed>' panicked at 'called `Option::unwrap()` on a `None` value', src/feed_reader.rs:40:59

Utilisez std::Option::unwrap_or_else pour définir une chaîne vide comme valeur par défaut. La syntaxe nécessite une fermeture qui renvoie une instanciation de structure Text vide.

J'ai dû m'y reprendre à plusieurs fois pour trouver l'initialisation correcte permettant de résoudre le problème, car le passage d'une chaîne vide ne fonctionne pas avec les types personnalisés. Je vais vous montrer mes tentatives et recherches.

// Problem: The `summary` attribute is not always initialized. unwrap() will panic! then.
// Requires use mime; and use feed_rs::model::Text;
/*
// 1st attempt: Use unwrap() to extraxt Text from Option<Text> type.
println!("Summary: {}", entry.summary.unwrap().content);
// 2nd attempt. Learned about unwrap_or_else, passing an empty string.
println!("Summary: {}", entry.summary.unwrap_or_else(|| "").content);
// 3rd attempt. summary is of the Text type, pass a new struct instantiation.
println!("Summary: {}", entry.summary.unwrap_or_else(|| Text{}).content);
// 4th attempt. Struct instantiation requires 3 field values.
println!("Summary: {}", entry.summary.unwrap_or_else(|| Text{"", "", ""}).content);
// 5th attempt. Struct instantation with public fields requires key: value syntax
println!("Summary: {}", entry.summary.unwrap_or_else(|| Text{content_type: "", src: "", content: ""}).content);
// 6th attempt. Reviewed expected Text types in https://docs.rs/feed-rs/latest/feed_rs/model/struct.Text.html and created Mime and String objects
println!("Summary: {}", entry.summary.unwrap_or_else(|| Text{content_type: mime::TEXT_PLAIN, src: String::new(), content: String::new()}).content);
// 7th attempt: String and Option<String> cannot be casted automagically. Compiler suggested using `Option::Some()`.
println!("Summary: {}", entry.summary.unwrap_or_else(|| Text{content_type: mime::TEXT_PLAIN, src: Option::Some(), content: String::new()}).content);
*/

// xth attempt: Solution. Option::Some() requires a new String object.
println!("Summary: {}", entry.summary.unwrap_or_else(|| Text{content_type: mime::TEXT_PLAIN, src: Option::Some(String::new()), content: String::new()}).content);

Cette approche n'était pas satisfaisante, car la ligne de code était difficile à lire et nécessitait une retouche manuelle sans suggestions de code. Puis, j'ai trouvé où se nichait le problème : si Option a la valeur none, unwrap() génère une erreur. J'ai demandé aux suggestions de code s’il existe une méthode plus simple, avec ce nouveau commentaire :

                // xth attempt: Solution. Option::Some() requires a new String object.
                println!("Summary: {}", entry.summary.unwrap_or_else(|| Text{content_type: mime::TEXT_PLAIN, src: Option::Some(String::new()), content: String::new()}).content);

                // Alternatively, use Option.is_none()

Demande aux suggestions de code d'une alternative pour Options.is_none

Cette méthode améliore la lisibilité, moins de cycles CPU sont gaspillés sur unwrap() et la courbe d'apprentissage est excellente, de la résolution d'un problème complexe à l'utilisation d'une solution standard. Gagnant-gagnant.

Ajoutez le stockage des données XML sur le disque pour finaliser l'application de lecteur de flux.

                // Dump the parsed body to a file, as name-current-iso-date.xml
                let file_name = format!("{}-{}.xml", thread_name, chrono::Local::now().format("%Y-%m-%d-%H-%M-%S"));
                let mut file = std::fs::File::create(file_name).unwrap();
                file.write_all(body.as_ref()).unwrap();

Compilez et exécutez le programme pour vérifier les données de sortie.

cargo build

time cargo run -- "GitLab Blog,https://about.gitlab.com/atom.xml" "CNCF,https://www.cncf.io/feed/"

Terminal VS Code, exécution de Cargo avec des données de sortie d'intrants au format requis

Benchmarks

Exécution séquentielle vs parallèle

Comparez les temps d'exécution du code en créant cinq échantillons pour chaque type d'exécution.

  1. Exécution séquentielle. Exemple de code source de la MR
  2. Exécution parallèle. Exemple de code source de la MR
# Sequential
git checkout sequential-exec

time cargo run -- "GitLab Blog,https://about.gitlab.com/atom.xml" "CNCF,https://www.cncf.io/feed/"

0.21s user 0.08s system 10% cpu 2.898 total
0.21s user 0.08s system 11% cpu 2.585 total
0.21s user 0.09s system 10% cpu 2.946 total
0.19s user 0.08s system 10% cpu 2.714 total
0.20s user 0.10s system 10% cpu 2.808 total
# Parallel
git checkout parallel-exec

time cargo run -- "GitLab Blog,https://about.gitlab.com/atom.xml" "CNCF,https://www.cncf.io/feed/"

0.19s user 0.08s system 17% cpu 1.515 total
0.18s user 0.08s system 16% cpu 1.561 total
0.18s user 0.07s system 17% cpu 1.414 total
0.19s user 0.08s system 18% cpu 1.447 total
0.17s user 0.08s system 16% cpu 1.453 total

Avec quatre threads RSS en parallèle, le temps total est presque réduit de moitié, mais le CPU est plus sollicité. Optimisons le code et les fonctionnalités.

Actuellement, nous utilisons la version de débogage Cargo, mais pas encore les versions optimisées. L'exécution parallèle a ses limites : les points de terminaison HTTP peuvent atteindre leur débit maximum, et

chaque thread nécessite un changement de contexte dans le noyau, consommant des ressources. Lorsqu'un thread reçoit des ressources de calcul, d'autres sont mis en veille. Un trop grand nombre de threads peut ralentir le système. Pour éviter cela, utilisez des design patterns comme les files d'attente de travail. Un pool de threads récupère cette tâche pour une exécution asynchrone.

Rust garantir la synchronisation des données entre les threads à l'aide des canaux. Pour garantir un accès simultané aux données, des mutexes (exclusions mutuelles) fournissent des verrous sûrs.

CI/CD avec mise en cache Rust

Ajoutez la configuration CI/CD suivante dans le fichier .gitlab-ci.yml. Le job run-latest appelle cargo run avec des exemples d'URL de flux RSS et mesure le temps d'exécution en continu.

stages:
  - build
  - test
  - run

default:
  image: rust:latest
  cache:
    key: ${CI_COMMIT_REF_SLUG}
    paths:
      - .cargo/bin
      - .cargo/registry/index
      - .cargo/registry/cache
      - target/debug/deps
      - target/debug/build
    policy: pull-push

# Cargo data needs to be in the project directory for being cached.
variables:
  CARGO_HOME: ${CI_PROJECT_DIR}/.cargo

build-latest:
  stage: build
  script:
    - cargo build --verbose

test-latest:
  stage: build
  script:
    - cargo test --verbose

run-latest:
  stage: run
  script:
    - time cargo run -- "GitLab Blog,https://about.gitlab.com/atom.xml" "CNCF,https://www.cncf.io/feed/"

Pipelines GitLab CI/CD pour Rust, données de sortie de l'exécution Cargo

Étapes suivantes

Rédiger cet article a été un défi pour maîtriser les techniques avancées de Rust et optimiser les suggestions de code. Ces dernières sont très utiles pour générer rapidement du code, avec le contexte local et pour ajuster l'algorithme à mesure que vous écrivez le code. Dans cet article, j'ai partagé les défis et les solutions. L'exemple de code pour l'application de lecture est disponible dans le projet learn-rust-ai-app-reader.

L'analyse des flux RSS étant complexe, elle nécessite des structures de données, des requêtes HTTP externes et des optimisations parallèles. Alors, pourquoi ne pas utiliser la crate std::rss -- pour une exécution asynchrone avancée. Cela ne démontre pas toutes les fonctionnalités de Rust abordées dans cet article, mais vous pouvez essayer de réécrire le code avec la crate rss pour pratiquer l'exécution asynchrone.

Exercices d'apprentissage de l'exécution asynchrone

Cet article pose les bases pour explorer le stockage persistant et la présentation des données. Pour vous perfectionner votre application en Rust, voici quelques idées :

  1. Stockage des données : utilisez une base de données comme sqlite et suivez les mises à jour des flux RSS.
  2. Notifications : déclenchez des notifications dans Telegram, ou autres.
  3. Fonctionnalités : étendez les types de lecteurs aux API REST
  4. Configuration : intégrez des fichiers de configuration pour les flux RSS, les API, etc.
  5. Efficacité : ajoutez des filtres et des balises d'abonnement.
  6. Déploiement : utilisez un serveur Web, collectez les indicateurs avec Prometheus et déployez sur Kubernetes.

Nous aborderons certains de ces sujets dans un prochain article. En attendant, essayez d'implémenter des flux RSS existants et explorez d'autres bibliothèques Rust (crates).

Votre retour d'expérience

Lorsque vous utilisez les suggestions de code de GitLab Duo, n'hésitez pas à partager vos retours dans le ticket prévu à cet effet.

Votre avis nous intéresse

Cet article de blog vous a plu ou vous avez des questions ou des commentaires ? Partagez vos réflexions en créant un nouveau sujet dans le forum de la communauté GitLab. Partager votre expérience

Lancez-vous dès maintenant

Découvrez comment la plateforme DevSecOps unifiée de GitLab peut aider votre équipe.

Commencer un essai gratuit

Découvrez le forfait qui convient le mieux à votre équipe

En savoir plus sur la tarification

Découvrez ce que GitLab peut offrir à votre équipe

Échanger avec un expert