Rustでユーザーエンティティを作ってみよう
はじめに
Rustで何かが作りたいので、学習もかねて試しにユーザーエンティティを作ってみたいと思います!!
コードはこちらにあります。
最速で環境を構築する場合
クローンすればすぐ環境を作成できます。
git clone https://github.com/ao-39/rust_devcontainer.git -b user_entity_sample
VSCodeの左下の><
のメニューからコンテナーのリビルド
を選択すれば環境を立ち上げられます。
ユーザーエンティティの設計
箇条書きですが、簡単に書いていきます。
保持する情報
- id
ULID
変更不可
かぶり不可 - discriminator
ユーザー識別子
3~24文字、アルファベット、数字、-のみ
最初の文字はアルファベット
変更可能
かぶり不可 - name
ユーザー名
3~80文字
変更可能 - メールアドレス
変更可能
かぶり不可 - ホームページ
変更可能 - 作成日時
- 更新日時
実装してみる
ユーザーエンティティ
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,
}
}
}
エンティティオブジェクトは、メンバーに値オブジェクトを持ちます。
できるだけライブラリで済ませられる値オブジェクトはライブラリを使用していきます。
ユーザー識別子値オブジェクトのUserDiscriminator
とUserName
は文字列の制限なので自前で値オブジェクトを作ります。(これもライブラリで済ませられたらいいのですが、うまい方法が見つけられませんでした)
ライブラリの値オブジェクト
- ULID値オブジェクト
https://crates.io/crates/rusty_ulid - メールアドレス値オブジェクト
https://crates.io/crates/email_address - URL値オブジェクト
https://crates.io/crates/url - 日付値オブジェクト
https://crates.io/crates/chrono
エンティティのメンバーに関しては、id
とcreated_at
とupdate_at
はreadonlyのような制限を付けたかったのですが、pubをはずすとreadすらできなくなるためひとまず制限はかけないようにしました。
ユーザー識別子値オブジェクト
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
への変換の関数の実装など抜けている部分は今後追加していきたいです。
ユーザー名値オブジェクト
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を使わずにバリデーションを行っています。なんだかこれでは足りない気がするのですが、ひとまずこの辺にしておきます! ほかのモジュールを作っていったら気づくと思います!
動かしてみる
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を起動している場合)
おわりに
アプリケーションに仕上げているわけではないので、いろいろと不都合や不完全なところはあると思いますので、今後伸ばしていきたいと思います。
また、ほかのモジュールの実装も試していけたらと思います。
Discussion