1
0
お題は不問!Qiita Engineer Festa 2024で記事投稿!
Qiita Engineer Festa20242024年7月17日まで開催中!

Rust 100 Ex 🏃【16/37】 Errorトレイトと外部クレート ~依存はCargo.tomlに全部お任せ!~

Last updated at Posted at 2024-07-17

前の記事

全記事一覧

100 Exercise To Learn Rust 演習第16回になります!

今回の関連ページ

[05_ticket_v2/09_error_trait] Error トレイト

問題はこちらです。

lib.rs
// TODO: Implement `Debug`, `Display` and `Error` for the `TicketNewError` enum.
//  When implementing `Display`, you may want to use the `write!` macro from Rust's standard library.
//  The docs for the `std::fmt` module are a good place to start and look for examples:
//  https://doc.rust-lang.org/std/fmt/index.html#write

enum TicketNewError {
    TitleError(String),
    DescriptionError(String),
}

// TODO: `easy_ticket` should panic when the title is invalid, using the error message
//   stored inside the relevant variant of the `TicketNewError` enum.
//   When the description is invalid, instead, it should use a default description:
//   "Description not provided".
fn easy_ticket(title: String, description: String, status: Status) -> Ticket {
    todo!()
}

#[derive(Debug, PartialEq, Clone)]
struct Ticket {
    title: String,
    description: String,
    status: Status,
}

// ...省略...
全体
lib.rs
// TODO: Implement `Debug`, `Display` and `Error` for the `TicketNewError` enum.
//  When implementing `Display`, you may want to use the `write!` macro from Rust's standard library.
//  The docs for the `std::fmt` module are a good place to start and look for examples:
//  https://doc.rust-lang.org/std/fmt/index.html#write

enum TicketNewError {
    TitleError(String),
    DescriptionError(String),
}

// TODO: `easy_ticket` should panic when the title is invalid, using the error message
//   stored inside the relevant variant of the `TicketNewError` enum.
//   When the description is invalid, instead, it should use a default description:
//   "Description not provided".
fn easy_ticket(title: String, description: String, status: Status) -> Ticket {
    todo!()
}

#[derive(Debug, PartialEq, Clone)]
struct Ticket {
    title: String,
    description: String,
    status: Status,
}

#[derive(Debug, PartialEq, Clone)]
enum Status {
    ToDo,
    InProgress { assigned_to: String },
    Done,
}

impl Ticket {
    pub fn new(
        title: String,
        description: String,
        status: Status,
    ) -> Result<Ticket, TicketNewError> {
        if title.is_empty() {
            return Err(TicketNewError::TitleError(
                "Title cannot be empty".to_string(),
            ));
        }
        if title.len() > 50 {
            return Err(TicketNewError::TitleError(
                "Title cannot be longer than 50 bytes".to_string(),
            ));
        }
        if description.is_empty() {
            return Err(TicketNewError::DescriptionError(
                "Description cannot be empty".to_string(),
            ));
        }
        if description.len() > 500 {
            return Err(TicketNewError::DescriptionError(
                "Description cannot be longer than 500 bytes".to_string(),
            ));
        }

        Ok(Ticket {
            title,
            description,
            status,
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use common::{overly_long_description, overly_long_title, valid_description, valid_title};
    use static_assertions::assert_impl_one;

    #[test]
    #[should_panic(expected = "Title cannot be empty")]
    fn title_cannot_be_empty() {
        easy_ticket("".into(), valid_description(), Status::ToDo);
    }

    #[test]
    fn template_description_is_used_if_empty() {
        let ticket = easy_ticket(valid_title(), "".into(), Status::ToDo);
        assert_eq!(ticket.description, "Description not provided");
    }

    #[test]
    #[should_panic(expected = "Title cannot be longer than 50 bytes")]
    fn title_cannot_be_longer_than_fifty_chars() {
        easy_ticket(overly_long_title(), valid_description(), Status::ToDo);
    }

    #[test]
    fn template_description_is_used_if_too_long() {
        let ticket = easy_ticket(valid_title(), overly_long_description(), Status::ToDo);
        assert_eq!(ticket.description, "Description not provided");
    }

    #[test]
    fn display_is_correctly_implemented() {
        let ticket = Ticket::new("".into(), valid_description(), Status::ToDo);
        assert_eq!(format!("{}", ticket.unwrap_err()), "Title cannot be empty");
    }

    assert_impl_one!(TicketNewError: std::error::Error);
}

今回の問題は正直あんまり"旨味"を感じにくい気がします。というのも、自前定義したエラー用の列挙体に Error トレイトを実装する必要性が問題内にはないからです。

解説

Error トレイトは Debug トレイトと Display トレイトを要求するので、これらを実装することで Error トレイトを実装できます。

lib.rs
use std::error::Error;
use std::fmt;

#[derive(Debug)]
enum TicketNewError {
    TitleError(String),
    DescriptionError(String),
}

use TicketNewError::*;

impl fmt::Display for TicketNewError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let (TitleError(s) | DescriptionError(s)) = self;
        write!(f, "{}", s)
    }
}

impl Error for TicketNewError {}

fn easy_ticket(title: String, description: String, status: Status) -> Ticket {
    let ticket_w = Ticket::new(title.clone(), description, status.clone());

    match ticket_w {
        Ok(ticket) => ticket,
        Err(DescriptionError(_)) => {
            Ticket::new(title, "Description not provided".to_string(), status).unwrap()
        }
        t => t.unwrap(),
    }
}

本問題は次問題以降の布石的な感じですね。用意したエラー型が std::error::Error を実装してくれていたほうが、エラーレポートの観点で後々便利なので実装しておいた方が良い、というのは確かです...でもやっぱり今回みたいに丁寧に実装することはほぼないんだよなぁ...

[05_ticket_v2/10_packages] パッケージ (クレート群)

問題はこちらです。今回はめずらしく lib.rs が準備されておらず、 lib.rs を自分で用意して main.rs で読み込むという構造になっています。

main.rs
// This is a `main.rs` file, therefore `cargo` interprets this as the root of a binary target.

// TODO: fix this broken import. Create a new library target in the `src` directory.
//   The library target should expose a public function named `hello_world` that takes no arguments
//   and returns nothing.
use packages::hello_world;

// This is the entrypoint of the binary.
fn main() {
    hello_world();
}

なぜ急に「外部のパッケージを読み込む」という問題を解いているかというと、最終的に thiserror という便利パッケージを読み込むためですね。

解説

lib.rs を用意して、必要な関数を書き pub で公開します。

lib.rs
pub fn hello_world() {}

今回の問題は、 lib.rsmain.rs は別モジュール扱いされるという仕組みのお陰で成り立っている部分もありますね。 lib.rs 以下ではクレートのルートパスは crate なのですが、 main.rs から見た lib.rsCargo.tomlpackage.name が参照されるというのは、覚えておくと便利でしょう。

[05_ticket_v2/11_dependencies] 外部パッケージの利用

問題はこちらです。「サードパーティ製のクレートを使ってみよう!」という問題です!

lib.rs
// TODO: Add `anyhow` as a dependency of this project.
//  Don't touch this import!

// When you import a type (`Error`) from a dependency, the import path must start
// with the crate name (`anyhow`, in this case).
use anyhow::Error;

今回も例外的な問題で、編集対象は lib.rs ではなく外部依存クレートを管理するところである Cargo.toml になります!

Cargo.toml
[package]
name = "deps"
version = "0.1.0"
edition = "2021"

解説

[dependencies] の項目に anyhow = "..." を加えることで、サードパーティクレート anyhow が使用可能になります!

Cargo.toml
[package]
name = "deps"
version = "0.1.0"
edition = "2021"

+ [dependencies]
+ anyhow = "1.0.86"

Cargo.toml を直接編集するのではなく、 cargo add コマンドの使用をおすすめします。というのも、最新版を参照してくれるように書き込んでくれるためです。

bash
cargo add anyhow

VSCodeを使っているならば、 crates という拡張機能もお勧めです。 Cargo.toml ファイルを開いてワイルドカードを用いて anyhow = "*" のようにバージョンを記載し、横の✅️を押すと最新版をサジェストしてくれます。

ちなみにワイルドカードによるバージョン指定について、面倒ならばそれでも問題はないですが(その代わり Cargo.lock もちゃんと管理する必要があります)、なるべく再利用性や再現性を高めたいならしっかり具体的に指定することをオススメします。指定しておかないと後で後悔しがち...

anyhow

anyhowthiserrorと対をなすエラーハンドリングのためのクレートです。

thiserrorが丁寧にエラーを扱うためのクレートなら、anyhowはテキトーに(つまり、「とにかく」)エラーを扱うためのクレートです!

anyhow一番最強の構造体は anyhow::Result<T> でしょう。dyn std::error::Erro + 'static な任意のエラーを、全部統一的に扱えてしまう恐ろしい構造体です...!

Result 周りで横着したくなった時は anyhow::Result を使ってみてください。飛ぶぞ

では次の問題に行きましょう!

次の記事: 【17】 thiserror・TryFrom ~トレイトもResultも自由自在!~

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0