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

معرفی
من یک توسعه دهنده rust متوسط هستم و ممکن است من را از پست های من در subreddits مانند r/opensource یا r/rust یا از پروژه های مختلف من در GitHub بشناسید.
اخیراً تصمیم گرفتیم که خطاها را مدیریت کنیم و پیامهای خطای سفارشی برای خطاهای مربوط به هر کد موتور موجود در زیر ارائه کنیم. src/engines
پوشه ای در یکی از پروژه های من websurfx هنگام استفاده از error-stack و ما می خواستیم از enums برای آن استفاده کنیم و متوجه شدیم که پروژه error-stack هیچ راهنما، آموزش یا مثالی ارائه نمی دهد، اما به لطف یکی از نگهبانان ما @xffxff، @xffxff یک ارائه داد. راه حل بسیار جالبی برای این مشکل و به من کمک کرد تا چیزهای زیادی یاد بگیرم، بنابراین تصمیم گرفتم آنچه را که در این پست از آن یاد گرفتم با همه شما به اشتراک بگذارم و اینکه چگونه می توانید هنگام استفاده از error-stack خطاها را با enums بپیچید، بنابراین تا پایان این مقاله را حفظ کنید. پست.
برای این آموزش، من یک برنامه خراش دادن ساده برای خراش دادن صفحه وب 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 پروژه ما بپیوندید.