1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Rust 100 Ex 🏃【17/37】 thiserror・TryFrom ~トレイトもResultも自由自在!~

Last updated at Posted at 2024-07-17

前の記事

全記事一覧

100 Exercise To Learn Rust 演習第17回になります!今回から投稿マラソンの関係で問題数を3つから2つに減らします、ご了承ください。

今回の関連ページ

[05_ticket_v2/12_thiserror] 便利クレート thiserror

問題はこちらです。

lib.rs
// TODO: Implement the `Error` trait for `TicketNewError` using `thiserror`.
//   We've changed the enum variants to be more specific, thus removing the need for storing
//   a `String` field into each variant.
//   You'll also have to add `thiserror` as a dependency in the `Cargo.toml` file.

enum TicketNewError {
    TitleCannotBeEmpty,
    TitleTooLong,
    DescriptionCannotBeEmpty,
    DescriptionTooLong,
}

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

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

// ...省略...
テストを含めた全体
lib.rs
// TODO: Implement the `Error` trait for `TicketNewError` using `thiserror`.
//   We've changed the enum variants to be more specific, thus removing the need for storing
//   a `String` field into each variant.
//   You'll also have to add `thiserror` as a dependency in the `Cargo.toml` file.

enum TicketNewError {
    TitleCannotBeEmpty,
    TitleTooLong,
    DescriptionCannotBeEmpty,
    DescriptionTooLong,
}

#[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::TitleCannotBeEmpty);
        }
        if title.len() > 50 {
            return Err(TicketNewError::TitleTooLong);
        }
        if description.is_empty() {
            return Err(TicketNewError::DescriptionCannotBeEmpty);
        }
        if description.len() > 500 {
            return Err(TicketNewError::DescriptionTooLong);
        }

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

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

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

    #[test]
    fn description_cannot_be_empty() {
        let err = Ticket::new(valid_title(), "".into(), Status::ToDo).unwrap_err();
        assert_eq!(err.to_string(), "Description cannot be empty");
    }

    #[test]
    fn title_cannot_be_longer_than_fifty_chars() {
        let err = Ticket::new(overly_long_title(), valid_description(), Status::ToDo).unwrap_err();
        assert_eq!(err.to_string(), "Title cannot be longer than 50 bytes");
    }

    #[test]
    fn description_cannot_be_too_long() {
        let err = Ticket::new(valid_title(), overly_long_description(), Status::ToDo).unwrap_err();
        assert_eq!(
            err.to_string(),
            "Description cannot be longer than 500 bytes"
        );
    }
}

前回複数のエクササイズに跨いで準備してきましたが、今回ようやく thiserror クレートを導入します!

解説

何はともあれまずは cargo add コマンドです。

bash
cargo add thiserror

これで dependenciesthiserror が加わります。

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

[dependencies]
+ thiserror = "1.0.61"

[dev-dependencies]
common = { path = "../../../helpers/common" }

(上記で dev-dependencies は、テストのみに必要な依存を書くところになります。)

thiserror クレートが提供するderiveマクロ Error が使用できるようになるので、これを利用して Error トレイトを実装します。

lib.rs
#[derive(thiserror::Error, Debug)]
enum TicketNewError {
    #[error("Title cannot be empty")]
    TitleCannotBeEmpty,
    #[error("Title cannot be longer than 50 bytes")]
    TitleTooLong,
    #[error("Description cannot be empty")]
    DescriptionCannotBeEmpty,
    #[error("Description cannot be longer than 500 bytes")]
    DescriptionTooLong,
}

これだけで エラー文付きで Error トレイトを実装 してくれます! Clone トレイトの時にもあったメリットですが、deriveマクロを使うことで、「楽にトレイトを実装できる」他、「宣言的に、漏れなく実装できる」というメリットがあります!

裏で何をしてくれているかを調べるためにここまでの演習がありましたが、意味がわかった今はマクロを使っていきたいです。

[05_ticket_v2/13_try_from] TryFrom トレイト

問題はこちらです。

lib.rs
// TODO: Implement `TryFrom<String>` and `TryFrom<&str>` for `Status`.
//  The parsing should be case-insensitive.

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

#[cfg(test)]
mod tests {
    use super::*;
    use std::convert::TryFrom;

    #[test]
    fn test_try_from_string() {
        let status = Status::try_from("ToDO".to_string()).unwrap();
        assert_eq!(status, Status::ToDo);

        let status = Status::try_from("inproGress".to_string()).unwrap();
        assert_eq!(status, Status::InProgress);

        let status = Status::try_from("Done".to_string()).unwrap();
        assert_eq!(status, Status::Done);
    }

    #[test]
    fn test_try_from_str() {
        let status = Status::try_from("todo").unwrap();
        assert_eq!(status, Status::ToDo);

        let status = Status::try_from("inprogress").unwrap();
        assert_eq!(status, Status::InProgress);

        let status = Status::try_from("done").unwrap();
        assert_eq!(status, Status::Done);
    }
}

解説

TryFrom/TryInto は、キャスト用トレイト From/Into の失敗する可能性がある版です!関連型としてエラーの時に返す型を指定し、対応する Result 型を返す処理を実装します。

lib.rs
use Status::*;

fn str2status(value: &str) -> Result<Status, String> {
    match value.to_lowercase().as_str() {
        "todo" => Ok(ToDo),
        "inprogress" => Ok(InProgress),
        "done" => Ok(Done),
        s => Err(s.to_string()),
    }
}

impl TryFrom<&str> for Status {
    type Error = String;

    fn try_from(value: &str) -> Result<Self, Self::Error> {
        str2status(value)
    }
}

impl TryFrom<String> for Status {
    type Error = String;

    fn try_from(value: String) -> Result<Self, Self::Error> {
        str2status(&value)
    }
}

String から Status への変換は今までのエクササイズでも似たような処理がしばしば登場していましたが、

  • 列挙型の使用
  • TryFrom による実装

によってかなり慣例的で読みやすい形となりました!たとえば String::from("ToDo").try_into()? と書いてあれば、他のプログラマはこの部分を見ただけで「ここは文字列からの失敗する可能性のあるキャストなんだな」とわかります。

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

次の記事: 【18】 Errorのネスト・慣例的な書き方 ~Rustらしさの目醒め~

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?