🚶‍♂️

Rustでユーザーエンティティを作ってみよう

2023/09/01に公開

はじめに

Rustで何かが作りたいので、学習もかねて試しにユーザーエンティティを作ってみたいと思います!!

コードはこちらにあります。

https://github.com/ao-39/rust_devcontainer/tree/user_entity_sample

最速で環境を構築する場合

クローンすればすぐ環境を作成できます。

git clone https://github.com/ao-39/rust_devcontainer.git -b user_entity_sample

VSCodeの左下の><のメニューからコンテナーのリビルドを選択すれば環境を立ち上げられます。

ユーザーエンティティの設計

箇条書きですが、簡単に書いていきます。
保持する情報

  • id
    ULID
    変更不可
    かぶり不可
  • discriminator
    ユーザー識別子
    3~24文字、アルファベット、数字、-のみ
    最初の文字はアルファベット
    変更可能
    かぶり不可
  • name
    ユーザー名
    3~80文字
    変更可能
  • メールアドレス
    変更可能
    かぶり不可
  • ホームページ
    変更可能
  • 作成日時
  • 更新日時

実装してみる

ユーザーエンティティ

app/domain/src/entity/user.rs
use chrono::{DateTime, Local};
use email_address::EmailAddress;
use rusty_ulid::Ulid;
use url::Url;

use serde::{Deserialize, Serialize};

use crate::object::{UserDiscriminator, UserName};

#[derive(Serialize, Deserialize, Debug)]
pub struct User {
    pub id: Ulid,
    pub discriminator: UserDiscriminator,
    pub name: UserName,
    pub email: EmailAddress,
    pub web_page: Url,
    pub created_at: DateTime<Local>,
    pub updated_at: DateTime<Local>,
}

impl User {
    pub fn new(
        id: Ulid,
        discriminator: UserDiscriminator,
        name: UserName,
        email: EmailAddress,
        web_page: Url,
        created_at: DateTime<Local>,
        updated_at: DateTime<Local>,
    ) -> Self {
        Self {
            id,
            discriminator,
            name,
            email,
            web_page,
            created_at,
            updated_at,
        }
    }
}

エンティティオブジェクトは、メンバーに値オブジェクトを持ちます。
できるだけライブラリで済ませられる値オブジェクトはライブラリを使用していきます。
ユーザー識別子値オブジェクトのUserDiscriminatorUserNameは文字列の制限なので自前で値オブジェクトを作ります。(これもライブラリで済ませられたらいいのですが、うまい方法が見つけられませんでした)

ライブラリの値オブジェクト

エンティティのメンバーに関しては、idcreated_atupdate_atはreadonlyのような制限を付けたかったのですが、pubをはずすとreadすらできなくなるためひとまず制限はかけないようにしました。

ユーザー識別子値オブジェクト

app/domain/src/object/user_discriminator.rs
use once_cell::sync::Lazy;
use regex::Regex;
use serde::{Deserialize, Serialize};

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct UserDiscriminator(String);

impl UserDiscriminator {
    pub fn new(discriminator: String) -> Result<Self, String> {
        static RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"^[a-zA-Z][a-zA-Z0-9-]{2,23}$").unwrap());
        if RE.is_match(&discriminator) {
            Ok(Self(discriminator))
        } else {
            Err("バリデーションエラー".to_string())
        }
    }
}

newの関数内でバリデーションを実施しています。エラーオブジェクトの扱いやStringへの変換の関数の実装など抜けている部分は今後追加していきたいです。

ユーザー名値オブジェクト

app/domain/src/object/user_name.rs
use serde::{Deserialize, Serialize};

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct UserName(String);

impl UserName {
    pub fn new(name: String) -> Result<Self, String> {
        if name.len() >= 3 && name.len() <= 80 {
            Ok(Self(name))
        } else {
            return Err("バリデーションエラー".to_string());
        }
    }
}

こちらではRegexを使わずにバリデーションを行っています。なんだかこれでは足りない気がするのですが、ひとまずこの辺にしておきます! ほかのモジュールを作っていったら気づくと思います!

動かしてみる

app/domain/examples/create_user.rs
use std::str::FromStr;
use tracing::info;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let user = domain::entity::User::new(
        rusty_ulid::Ulid::generate(),
        domain::object::UserDiscriminator::new("john".to_string())?,
        domain::object::UserName::new("John Doe".to_string())?,
        email_address::EmailAddress::from_str("example@example.com")?,
        url::Url::parse("https://example.com")?,
        chrono::Local::now(),
        chrono::Local::now(),
    );
    info!("{:?}", user);
    Ok(())
}

./appに移動して以下のコマンドを実行するか、VSCodeのfn main()の上に表示されるRunをクリックすることで実行できます。

cargo run --package domain --example create_user 

VSCodeのfn main()の上に表示されるDebugをクリックすることでデバッガーを起動することができます(クローンしてdevcontainerを起動している場合)

おわりに

アプリケーションに仕上げているわけではないので、いろいろと不都合や不完全なところはあると思いますので、今後伸ばしていきたいと思います。
また、ほかのモジュールの実装も試していけたらと思います。

コラボスタイル Developers

Discussion