برنامه نویسی

چگونه هنگام استفاده از Error-stack، خطاهای خود را با Enums بپیچید

معرفی

من یک توسعه دهنده rust متوسط ​​هستم و ممکن است من را از پست های من در subreddits مانند r/opensource یا r/rust یا از پروژه های مختلف من در GitHub بشناسید.

اخیراً تصمیم گرفتیم که خطاها را مدیریت کنیم و پیام‌های خطای سفارشی برای خطاهای مربوط به هر کد موتور موجود در زیر ارائه کنیم. src/engines پوشه ای در یکی از پروژه های من websurfx هنگام استفاده از error-stack و ما می خواستیم از enums برای آن استفاده کنیم و متوجه شدیم که پروژه error-stack هیچ راهنما، آموزش یا مثالی ارائه نمی دهد، اما به لطف یکی از نگهبانان ما @xffxff، @xffxff یک ارائه داد. راه حل بسیار جالبی برای این مشکل و به من کمک کرد تا چیزهای زیادی یاد بگیرم، بنابراین تصمیم گرفتم آنچه را که در این پست از آن یاد گرفتم با همه شما به اشتراک بگذارم و اینکه چگونه می توانید هنگام استفاده از error-stack خطاها را با enums بپیچید، بنابراین تا پایان این مقاله را حفظ کنید. پست.

شخصی که تخته چوبی را با و لبه 1 می کند

برای این آموزش، من یک برنامه خراش دادن ساده برای خراش دادن صفحه وب example.com خواهم نوشت و سپس کدی را برای رسیدگی به خطاها با enums بر اساس آن می نویسیم.

بیایید مستقیم در آن شیرجه بزنیم

برنامه خراش دادن ساده

بیایید ابتدا با توضیح کدی که برای خراش دادن آن نوشته ام شروع کنیم More information href link در صفحه وب example.com که از طریق برنامه برای نوشتن استفاده خواهیم کرد error-handling کد برای آن این هم کد:

//! The main module that fetches html code and scrapes the more information href link and displays
//! it on stdout.

use reqwest::header::{HeaderMap, CONTENT_TYPE, REFERER, USER_AGENT};
use scraper::{Html, Selector};
use std::{println, time::Duration};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // A map that holds various request headers
    let mut headers = HeaderMap::new();
    headers.insert(
        USER_AGENT,
        "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:105.0) Gecko/20100101 Firefox/105.0"
            .parse()?, // --> (1)
    );
    headers.insert(REFERER, "https://google.com/".parse()?); // --> (1)
    headers.insert(CONTENT_TYPE, "application/x-www-form-urlencoded".parse()?); // --> (1)

    // A blocking request call that fetches the html text from the example.com site.
    let html_results = reqwest::blocking::Client::new()
        .get("https://example.com/")
        .timeout(Duration::from_secs(30))
        .headers(headers)
        .send()? // --> (2)
        .text()?; // --> (2)

    // Parse the recieved html text to html for scraping.
    let document = Html::parse_document(&html_results);

    // Initialize a new selector for scraping more information href link.
    let more_info_href_link_selector = Selector::parse("div>p>a")?; // --> (3)

    // Scrape the more information href link.
    let more_info_href_link = document
        .select(&more_info_href_link_selector)
        .next()
        .unwrap()
        .value()
        .attr("href")
        .unwrap();

    // Print the more information link.
    println!("More information link: {}", more_info_href_link);

    Ok(())
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

در Cargo.toml فایل شما باید چیزی شبیه به این را در بخش وابستگی ها ارائه دهید:

[dependencies]
scraper="0.16.0"
error-stack="0.3.1"
reqwest = {version="0.11.18",features=["blocking"]}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

اکنون می دانم که کد بالا ممکن است ترسناک و دلهره آور به نظر برسد، اما شما نیازی به تمرکز بر روی پیاده سازی ندارید، زیرا مهم نیست که کد چیست، به همین دلیل است که من بخش هایی را که برای این آموزش مهم هستند شماره گذاری کرده ام.

شاید بپرسید what are the those question marks in the numbered parts?? و what it does??.

اپراتور علامت سوال چیست؟؟

با توجه به مورد علاقه ما به منبع کتاب rust lang بروید:

عملگر علامت سوال (?) مقادیر معتبر را باز می کند یا مقادیر اشتباه را برمی گرداند و آنها را به تابع فراخوانی ارسال می کند. این یک عملگر واحد است که فقط می تواند برای انواع Result و Option اعمال شود

بگذارید در آن به طور مختصر توضیح دهم.

یک مهندس از طریق ارائه توضیح می دهد

را question mark operator (?) در Rust اجازه می دهد تا هر عملیاتی را که یک نوع نتیجه را با مقدار یا خطا برمی گرداند، انجام دهیم، اگر اتفاق بدی با مهربانی بیشتری مدیریت شود، مانند اگر عملیات با موفقیت کامل شود، اجرای بقیه تابع یا برنامه ادامه می یابد، در غیر این صورت به تابعی که آن را فراخوانی کرده و از اجرای بقیه تابع خارج می شود. match بیانیه یا if let از طرف دیگر اگر قرار بود عملیات در داخل تابع تماس گیرنده انجام شود main تابع، پس اگر عملیات شکست بخورد، مانند قبل، بقیه کدها هرگز اجرا نمی شوند و خطا به standard output (stdout) و خاموش می شود stdout.

به عنوان مثال، این کد زنگ زدگی را در نظر بگیرید:

fn main() {
    match caller("4", "6") {
        Ok(sum) => println!("Sum is: {}", sum),
        Err(error) => println!("{}", error),
    }
}

fn sum_numbers_from_string(
    number_x_as_string: &str,
    number_y_as_string: &str,
) -> Result<u32, Box<dyn std::error::Error>> {
    let number_x: u32 = number_x_as_string.parse()?; 
    let number_y: u32 = number_y_as_string.parse()?;

    println!("This code is being executed!! and the code below will also be executed!! :)");

    Ok(number_x + number_y)
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

در اینجا می توانید ببینید main تابع تابع را فرا می خواند sum_numbers_from_string که اگر کد با عملگر علامت سوال به اشتباه وارد شود، اکنون یک نوع نتیجه را برمی گرداند sum_numbers_from_string سپس اجرا به درستی متوقف خواهد شد

وجود دارد و کد زیر آن شامل دستور println!() هرگز اجرا نخواهد شد و یک ParseError به main تابعی که سپس با عبارت match و the مطابقت داده می شود Err دسته اجرا خواهد شد.

حالا بیایید با قرار دادن عملیات با the مثال دیگری بزنیم question mark operator در عملکرد اصلی کدی که برای آن چیزی شبیه به این خواهد بود:

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let number_x: u32 = "4".parse()?;
    let number_y: u32 = "6".parse()?;

    println!("This code is being executed!! and the code below will also be executed!! :)");

    println!("Sum is: {}", number_x + number_y);

    Ok(())
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

حال اگر قرار بود کد بالا اجرا شود و عملیات با question mark operator در صورت عدم موفقیت، طبق معمول برنامه اجرا متوقف می شود و خطا به آن منتقل می شود stdout و روی ترمینال چاپ می شود.

توجه داشته باشید
می‌دانم که بسیاری از جزئیات دقیق‌تر را از دست داده‌ام، اما به خاطر درک و سادگی این کار را عمدا انجام داده‌ام، اما اگر می‌خواهید عمیق‌تر در مورد آن بدانید، خواندن این پست وبلاگ را توصیه می‌کنم.

تصویر تجزیه و تحلیل کد

“کد تجزیه و تحلیل و توضیح داده شد”

نوشتن کد برای رسیدگی به خطاها با Enums

قبل از شروع، اجازه دهید به طور خلاصه به این موضوع بپردازیم که هر عملیات با چه چیزی انجام می شود question mark operator در هر قسمت شماره گذاری شده برمی گردد.

برای اولین قسمت شماره گذاری شده، عملیات یک نوع Result چیزی شبیه به این می دهد Result<HeaderValue, InvalidHeaderValue> همانطور که اکنون می دانید اگر این عملیات با شکست مواجه شود InvalidHeaderValue توسط main تابع به stdout و بر روی آن نمایش داده خواهد شد. به طور مشابه، بخش دوم و سوم انواع Result را برمی‌گردانند Result<String, ReqwestError> و Result<Selector, SelectorErrorKind> به ترتیب.

اکنون همانطور که می دانیم هر قسمت چه چیزی را برمی گرداند، می توانیم شروع به نوشتن کد کنیم تا هنگام استفاده، این خطاها را با enums بپیچیم error-stack crate ابتدا با نوشتن خطای enum شروع می کنیم و آن را فراخوانی می کنیم ScraperError.

#[derive(Debug)]
enum ScraperError {
    InvalidHeaderMapValue,
    RequestError,
    SelectorError,
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

سپس باید دو صفت را بر روی خطای خود با تعداد آن پیاده سازی کنیم Display و Context کدی که به نظر می رسد چیزی شبیه به این است:

impl fmt::Display for ScraperError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            ScraperError::InvalidHeaderMapValue => {
                write!(f, "Invalid header map value provided")
            }
            ScraperError::RequestError => {
                write!(f, "Error occurred while requesting data from the webpage")
            }
            ScraperError::SelectorError => {
                write!(f, "An error occured while initializing new Selector")
            }
        }
    }
}

impl Context for ScraperError {}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

با پیاده سازی Display ویژگی ما برای هر نوع خطایی که با آن مواجه می شود پیام های خطای مناسب و با اجرای Context صفت به خطای enum این قابلیت را می دهیم که به a تبدیل شود Report تایپ کنید در غیر این صورت اگر این اجرا نشد، برنامه منجر به خطای زمان کامپایل می شود که بیان می کند the following cannot be converted into a Report type.

اکنون باید هر کدام را جایگزین کنیم Question mark operator و نوع برگشتی را تغییر دهید main تابع به Result<(), ScraperError> بنابراین کد چیزی شبیه به این خواهد بود:

fn main() -> Result<(), ScraperError> {
    // A map that holds various request headers
    let mut headers = HeaderMap::new();
    headers.insert(
        USER_AGENT,
        "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:105.0) Gecko/20100101 Firefox/105.0"
            .parse()
            .into_report()
            .change_context(ScraperError::InvalidHeaderMapValue)?, // --> (1)
    );
    headers.insert(
        REFERER,
        "https://google.com/"
            .parse()
            .into_report()
            .change_context(ScraperError::InvalidHeaderMapValue)?,
    ); // --> (1)
    headers.insert(
        CONTENT_TYPE,
        "application/x-www-form-urlencoded"
            .parse()
            .into_report()
            .change_context(ScraperError::InvalidHeaderMapValue)?,
    ); // --> (1)

    // A blocking request call that fetches the html text from the example.com site.
    let html_results = reqwest::blocking::Client::new()
        .get("https://example.com/")
        .timeout(Duration::from_secs(30))
        .headers(headers)
        .send()
        .into_report()
        .change_context(ScraperError::RequestError)? // --> (2)
        .text()
        .into_report()
        .change_context(ScraperError::RequestError)?; // --> (2)

    // Parse the recieved html text to html for scraping.
    let document = Html::parse_document(&html_results);

    // Initialize a new selector for scraping more information href link.
    let more_info_href_link_selector = Selector::parse("div>p>a$")
        .into_report()
        .change_context(ScraperError::SelectorError)?; // --> (3)

    // Scrape the more information href link.
    let more_info_href_link = document
        .select(&more_info_href_link_selector)
        .next()
        .unwrap()
        .value()
        .attr("href")
        .unwrap();

    // Print the more information link.
    println!("More information link: {}", more_info_href_link);

    Ok(())
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

قرار دادن آن در کل. کد به شکل زیر است:

//! The main module that fetches html code and scrapes the more information href link and displays
//! it on stdout.

use core::fmt;
use error_stack::{Context, IntoReport, Result, ResultExt};
use reqwest::header::{HeaderMap, CONTENT_TYPE, REFERER, USER_AGENT};
use scraper::{Html, Selector};
use std::{println, time::Duration};

#[derive(Debug)]
enum ScraperError {
    InvalidHeaderMapValue,
    RequestError,
    SelectorError,
}

impl fmt::Display for ScraperError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            ScraperError::InvalidHeaderMapValue => {
                write!(f, "Invalid header map value provided")
            }
            ScraperError::RequestError => {
                write!(f, "Error occurred while requesting data from the webpage")
            }
            ScraperError::SelectorError => {
                write!(f, "An error occured while initializing new Selector")
            }
        }
    }
}

impl Context for ScraperError {}

fn main() -> Result<(), ScraperError> {
    // A map that holds various request headers
    let mut headers = HeaderMap::new();
    headers.insert(
        USER_AGENT,
        "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:105.0) Gecko/20100101 Firefox/105.0"
            .parse()
            .into_report()
            .change_context(ScraperError::InvalidHeaderMapValue)?, // --> (1)
    );
    headers.insert(
        REFERER,
        "https://google.com/"
            .parse()
            .into_report()
            .change_context(ScraperError::InvalidHeaderMapValue)?,
    ); // --> (1)
    headers.insert(
        CONTENT_TYPE,
        "application/x-www-form-urlencoded"
            .parse()
            .into_report()
            .change_context(ScraperError::InvalidHeaderMapValue)?,
    ); // --> (1)

    // A blocking request call that fetches the html text from the example.com site.
    let html_results = reqwest::blocking::Client::new()
        .get("https://example.com/")
        .timeout(Duration::from_secs(30))
        .headers(headers)
        .send()
        .into_report()
        .change_context(ScraperError::RequestError)? // --> (2)
        .text()
        .into_report()
        .change_context(ScraperError::RequestError)?; // --> (2)

    // Parse the recieved html text to html for scraping.
    let document = Html::parse_document(&html_results);

    // Initialize a new selector for scraping more information href link.
    let more_info_href_link_selector = Selector::parse("div>p>a$")
        .into_report()
        .change_context(ScraperError::SelectorError)?; // --> (3)

    // Scrape the more information href link.
    let more_info_href_link = document
        .select(&more_info_href_link_selector)
        .next()
        .unwrap()
        .value()
        .attr("href")
        .unwrap();

    // Print the more information link.
    println!("More information link: {}", more_info_href_link);

    Ok(())
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

واکنش جیمز ارل جونز گیف

“کد بالا را با هیجان اجرا نکنید وگرنه از کار نکردن آن شوکه خواهید شد”

اما هنوز هیجان زده نشوید زیرا وقتی کد بالا را اجرا می کنید یک خطای زمان کامپایل ترسناک به شرح زیر ایجاد می کند:

error[E0599]: the method `into_report` exists for enum `Result<Selector, SelectorErrorKind<'_>>`, but its trait bounds were not satisfied
    |
   ::: /home/destruct/.cargo/registry/src/github.com-1ecc6299db9ec823/error-stack-0.3.1/src/report.rs:249:1
    |
249 |   pub struct Report<C> {
    |   -------------------- doesn't satisfy `_: From<SelectorErrorKind<'_>>`
    |
   ::: /home/destruct/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:503:1
    |
503 |   pub enum Result<T, E> {
    |   --------------------- doesn't satisfy `_: IntoReport`
   --> src/main.rs:77:10
    |
76  |       let more_info_href_link_selector = Selector::parse("div>p>a$")
    |  ________________________________________-
77  | |         .into_report()
    | |_________-^^^^^^^^^^^
    |
    = note: the following trait bounds were not satisfied:
            `error_stack::Report<SelectorErrorKind<'_>>: From<SelectorErrorKind<'_>>`
            which is required by `Result<Selector, SelectorErrorKind<'_>>: IntoReport`

For more information about this error, try `rustc --explain E0599`.
error: could not compile `error-stack-blog` due to previous error
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

شخصی که با استفاده از ذره بین رمزگشایی می کند

“رمزگشایی خطای بالا”

اگر سعی کنید خطا را رمزگشایی کنید، بسیار گیج کننده است و واقعاً مشکل واقعی را توضیح نمی دهد. مشکل کد ما این است که خطا از آن برگردانده شده است Selector::parse() عملیات در قسمت سوم ایمن نیست.

رفع مشکل ایمنی نخ

برای رفع موارد فوق thread-safety خطا باید خطای عملیات انتخابگر قسمت سوم را با ساختن آن به نوع گزارش پشته خطا نگاشت کنیم. همچنین یک پیغام خطای سفارشی اضافه می کنیم که می خواهیم در صورت مواجه شدن با این خطا، آن را چاپ کنیم.

مردی که از آچار استفاده می کند

“رفع کد”

کد نگاشت خطا از عملیات قسمت سوم به صورت زیر خواهد بود:

let more_info_href_link_selector = Selector::parse("div>p>a$")
    .map_err(|_| Report::new(ScraperError::SelectorError))
    .attach_printable_lazy(|| "invalid CSS selector provided")?; // --> (3)
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

با قرار دادن آن در مجموع، کل کد به صورت زیر است:

//! The main module that fetches html code and scrapes the more information href link and displays
//! it on stdout.

use core::fmt;
use error_stack::{Context, IntoReport, Report, Result, ResultExt};
use reqwest::header::{HeaderMap, CONTENT_TYPE, REFERER, USER_AGENT};
use scraper::{Html, Selector};
use std::{println, time::Duration};

#[derive(Debug)]
enum ScraperError {
    InvalidHeaderMapValue,
    RequestError,
    SelectorError,
}

impl fmt::Display for ScraperError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            ScraperError::InvalidHeaderMapValue => {
                write!(f, "Invalid header map value provided")
            }
            ScraperError::RequestError => {
                write!(f, "Error occurred while requesting data from the webpage")
            }
            ScraperError::SelectorError => {
                write!(f, "An error occured while initializing new Selector")
            }
        }
    }
}

impl Context for ScraperError {}

fn main() -> Result<(), ScraperError> {
    // A map that holds various request headers
    let mut headers = HeaderMap::new();
    headers.insert(
        USER_AGENT,
        "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:105.0) Gecko/20100101 Firefox/105.0"
            .parse()
            .into_report()
            .change_context(ScraperError::InvalidHeaderMapValue)?, // --> (1)
    );
    headers.insert(
        REFERER,
        "https://google.com/"
            .parse()
            .into_report()
            .change_context(ScraperError::InvalidHeaderMapValue)?,
    ); // --> (1)
    headers.insert(
        CONTENT_TYPE,
        "application/x-www-form-urlencoded"
            .parse()
            .into_report()
            .change_context(ScraperError::InvalidHeaderMapValue)?,
    ); // --> (1)

    // A blocking request call that fetches the html text from the example.com site.
    let html_results = reqwest::blocking::Client::new()
        .get("https://example.com/")
        .timeout(Duration::from_secs(30))
        .headers(headers)
        .send()
        .into_report()
        .change_context(ScraperError::RequestError)? // --> (2)
        .text()
        .into_report()
        .change_context(ScraperError::RequestError)?; // --> (2)

    // Parse the recieved html text to html for scraping.
    let document = Html::parse_document(&html_results);

    // Initialize a new selector for scraping more information href link.
    let more_info_href_link_selector = Selector::parse("div>p>a$")
        .map_err(|_| Report::new(ScraperError::SelectorError))
        .attach_printable_lazy(|| "invalid CSS selector provided")?; // --> (3)

    // Scrape the more information href link.
    let more_info_href_link = document
        .select(&more_info_href_link_selector)
        .next()
        .unwrap()
        .value()
        .attr("href")
        .unwrap();

    // Print the more information link.
    println!("More information link: {}", more_info_href_link);

    Ok(())
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

توجه داشته باشید
در کد بالا من عمداً یک اشکال معرفی کرده ام که به ما امکان می دهد آزمایش کنیم که آیا پشته خطا با موفقیت پیاده سازی شده است یا خیر. ScraperError enum

با اجرای کد بالا، خواهید دید که کد همانطور که انتظار می رود اجرا می شود و a را پرتاب می کند ScraperError و با اولین پیامی که در حین پیاده سازی ارائه کرده بودیم یک خروجی خطای زیبا می دهد Display و آخرین پیامی که آن را به خطای ارائه شده توسط عملیات در قسمت سوم نگاشت کرده بودیم.

Error: An error occured while initializing new Selector
├╴at src/main.rs:77:22
╰╴invalid CSS selector provided
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

نتیجه

در نهایت این پست را به پایان می‌رسانیم زیرا همه مواردی را که برای پوشاندن خطاها با enums هنگام استفاده لازم بود پوشش داده‌ایم error-stack جعبه

من دوست دارم از شما بشنوم:

چه چیز جدیدی در کنار این پست یاد نمیگیرید؟؟

همچنین اگر این پست را مفید یافتید، آن را در شبکه های اجتماعی مختلف مانند توییتر، ردیت، لمی و غیره به اشتراک بگذارید.

با من در Reddit تماس بگیرید، جایی که من با نام کاربری آشنا هستم u/RevolutionaryAir1922 یا به من در Discord پیام دهید که در آن با نام کاربری آشنا هستم neon_mmd یا مرا در سرور Rust Discord تگ کنید.

اگر می خواهید با ما سر بزنید می توانید به سرور Discord پروژه ما بپیوندید.

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

دکمه بازگشت به بالا