Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

フォームドメインのリファクタリング #634

Merged
merged 51 commits into from
Jan 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
7d94ddc
refactor: Form ドメインから OffsetAndLimit struct を削除
rito528 Dec 15, 2024
9106120
refactor: Question struct に form_id フィールドを追加
rito528 Dec 15, 2024
2a265ab
refactor: Form struct から questions field を剥がす
rito528 Dec 15, 2024
eb18053
refactor: form struct から labels フィールドを剥がす
rito528 Dec 15, 2024
3897a30
refactor: フォームドメインとしての SimpleForm を廃止
rito528 Dec 15, 2024
34be9f2
refactor: フォームに紐づくデータ型を new type pattern で書き直す
rito528 Dec 15, 2024
603f0dc
refactor: Form 関連のデータ型にドメイン制約をかける
rito528 Dec 15, 2024
985ca3e
refactor: Form に AuthorizationGuardDefinitions を実装
rito528 Dec 15, 2024
53a8909
refactor: Form の read まわりに AuthorizationGuard をつける
rito528 Dec 16, 2024
741748d
refactor: フォームリスト用ハンドラを一つに統一する
rito528 Dec 17, 2024
e77fd39
refactor: フォームのドメインモデルの定義ファイルを分割
rito528 Dec 17, 2024
c191c6d
style: 定義の並べ替え
rito528 Dec 18, 2024
08d9759
refactor: form 周りの定義ファイルを分離
rito528 Dec 18, 2024
eaf00d4
refactor: FormLabel と AnswerLabel を分離
rito528 Dec 18, 2024
e511039
refactor: FormLabel のドメインモデルを再定義
rito528 Dec 20, 2024
575aeeb
refactor: CommentContent のドメインの再定義
rito528 Dec 20, 2024
46c32aa
feat: NonEmptyString を実装
rito528 Dec 20, 2024
c9e9adf
refactor: CommentContent を String から NonEmptyString にする
rito528 Dec 20, 2024
5be0024
refactor: Domain として Empty を許さない文字列を EmptyString 型に変更
rito528 Dec 20, 2024
d77d7e9
refactor: FormAnswer のドメインを再設計
rito528 Dec 20, 2024
588e6f5
refactor: DefaultAnswerTitleDomainService を実装
rito528 Dec 22, 2024
a790b84
test: embedded_answer_title のテストを追加
rito528 Dec 22, 2024
0b648c7
test: 動作しないテストコードを修正
rito528 Dec 22, 2024
f6c50d3
chore: Makefile に doc test を追加
rito528 Dec 22, 2024
1a64f19
chore: 古くなった FIXME を削除
rito528 Dec 22, 2024
648cd7e
refactor: FormAnswerContent から Clone の derive を消す
rito528 Dec 22, 2024
1506a1e
refactor: AnswerService に回答の送信をするサービスを実装
rito528 Dec 22, 2024
8e99f70
refactor: FormSettings と AnswerSettings を分ける
rito528 Dec 23, 2024
cdc312e
refactor: response_period 内か確認する関数を ResponsePeriod に実装
rito528 Dec 25, 2024
bcfbd80
refactor: FormAnswer モデルを再実装
rito528 Dec 26, 2024
151375a
docs: AuthorizationGuardDefinitions に想定している定義について追記
rito528 Dec 27, 2024
535e872
chore: FormAnswer -> AnswerEntry
rito528 Dec 28, 2024
cba5f64
feat: Verified 型を用いてAnswerEntryのPostを表現する
rito528 Dec 30, 2024
7e7ddef
chore: apply clippy fix
rito528 Jan 19, 2025
6066904
refactor: Verifier trait の verify 関数として T 型の引数を受け取るように
rito528 Jan 19, 2025
5cb7ed4
docs: 古くなったドキュメントの削除
rito528 Jan 19, 2025
b23f90e
feat: AuthorizationGuardWithContext の実装
rito528 Jan 23, 2025
aa01853
docs:AuthorizationGuardWithContext のドキュメントを更新
rito528 Jan 23, 2025
b009da8
feat: 回答の投稿実装で AuthorizationGuardWithContext を使うように
rito528 Jan 25, 2025
56c2c03
refactor: すべての回答を取得する処理で AuthorizationGuardWithContext を使う
rito528 Jan 25, 2025
985125a
refactor: 回答の取得時に AuthorizationGuardWithContext に AnwerEntry をくるんで返す
rito528 Jan 25, 2025
bfa23ac
refactor: Verified を削除
rito528 Jan 25, 2025
152fbb4
refactor: 回答情報の変更時に AuthorizationGuardWithContext を使用する
rito528 Jan 25, 2025
bc4e9dc
refactor: get_answers_by_form_id で返す AnswerEntry を AuthorizationGuard…
rito528 Jan 26, 2025
0b263ac
style: apply makers pretty
rito528 Jan 26, 2025
e955f49
refactor: FormAnswerContent に AuthorizationGuardWithContextDefinition…
rito528 Jan 26, 2025
4a4a18b
feat: Comment に AuthorizationGuardWithContextDefinitions を実装
rito528 Jan 26, 2025
9054e2b
refactor: Comment に AuthorizationGuardWithContext を実装
rito528 Jan 26, 2025
9090c4b
fix: Form 情報を返すスキーマがリファクタリング時に変わっていたのを直す
rito528 Jan 27, 2025
fb86051
refactor: embedded_answer_title -> generate_embedded_answer_title
rito528 Jan 27, 2025
f9afae2
refactor: 権限設定の意図を明確にする
rito528 Jan 27, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion server/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 8 additions & 1 deletion server/Makefile.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,18 @@ args = ["migrate", "generate", "${@}"]
command = "cargo"
args = ["clippy", "--fix", "--allow-dirty", "--allow-staged"]

[tasks.test]
[tasks.doctest]
command = "cargo"
args = ["test", "--doc"]

[tasks.nextest]
install_crate = { crate_name = "cargo-nextst" }
command = "cargo"
args = ["nextest", "run"]

[tasks.test]
dependencies = ["nextest", "doctest"]

[tasks.lint]
command = "cargo"
args = ["clippy", "--", "-D", "warnings"]
Expand Down
1 change: 1 addition & 0 deletions server/domain/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ typed-builder = "0.20.0"
types = { path = "../types" }
uuid = { workspace = true }
tokio = { workspace = true }
regex = { workspace = true }

[dev-dependencies]
chrono = { workspace = true, features = ["arbitrary"] }
Expand Down
5 changes: 5 additions & 0 deletions server/domain/src/form.rs
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
pub mod answer;
pub mod comment;
pub mod message;
pub mod models;
pub mod question;
pub mod service;
3 changes: 3 additions & 0 deletions server/domain/src/form/answer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pub mod models;
pub mod service;
pub mod settings;
94 changes: 94 additions & 0 deletions server/domain/src/form/answer/models.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
use chrono::{DateTime, Utc};
use derive_getters::Getters;
use deriving_via::DerivingVia;
#[cfg(test)]
use proptest_derive::Arbitrary;
use serde::{Deserialize, Serialize};
use types::non_empty_string::NonEmptyString;

use crate::{
form::{models::FormId, question::models::QuestionId},
user::models::User,
};

pub type AnswerId = types::Id<AnswerEntry>;

#[derive(Clone, DerivingVia, Default, Debug, PartialEq)]
#[deriving(From, Into, IntoInner, Serialize(via: Option::<NonEmptyString>), Deserialize(via: Option::<NonEmptyString>
))]
pub struct AnswerTitle(Option<NonEmptyString>);

impl AnswerTitle {
pub fn new(title: Option<NonEmptyString>) -> Self {
Self(title)
}
}

#[derive(Serialize, Deserialize, Getters, PartialEq, Debug)]
pub struct AnswerEntry {
id: AnswerId,
user: User,
timestamp: DateTime<Utc>,
form_id: FormId,
title: AnswerTitle,
}

impl AnswerEntry {
/// [`AnswerEntry`] を新しく作成します。
pub fn new(user: User, form_id: FormId, title: AnswerTitle) -> Self {
Self {
id: AnswerId::new(),
user,
timestamp: Utc::now(),
form_id,
title,
}
}

/// [`AnswerEntry`] の各フィールドを指定して新しく作成します。
///
/// # Safety
/// この関数はオブジェクトを新しく作成しない場合
/// (例えば、データベースから取得した場合)にのみ使用してください。
pub unsafe fn from_raw_parts(
id: AnswerId,
user: User,
timestamp: DateTime<Utc>,
form_id: FormId,
title: AnswerTitle,
) -> Self {
Self {
id,
user,
timestamp,
form_id,
title,
}
}

pub fn with_title(&self, title: AnswerTitle) -> Self {
Self {
id: self.id,
user: self.user.to_owned(),
form_id: self.form_id,
timestamp: self.timestamp,
title,
}
}
}

#[derive(Serialize, Deserialize, PartialEq, Debug)]
pub struct FormAnswerContent {
pub answer_id: AnswerId,
pub question_id: QuestionId,
pub answer: String,
}

pub type AnswerLabelId = types::IntegerId<AnswerLabel>;

#[cfg_attr(test, derive(Arbitrary))]
#[derive(Serialize, Deserialize, Debug, PartialEq)]
pub struct AnswerLabel {
pub id: AnswerLabelId,
pub name: String,
}
102 changes: 102 additions & 0 deletions server/domain/src/form/answer/service.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
use chrono::Utc;

use crate::{
form::{
answer::{
models::{AnswerEntry, FormAnswerContent},
settings::models::{AnswerVisibility, ResponsePeriod},
},
models::Visibility,
},
types::authorization_guard_with_context::{
Actions, AuthorizationGuardWithContext, AuthorizationGuardWithContextDefinitions,
},
user::models::{Role, User},
};

#[derive(Debug)]
pub struct AnswerEntryAuthorizationContext {
pub form_visibility: Visibility,
pub response_period: ResponsePeriod,
pub answer_visibility: AnswerVisibility,
}

// NOTE: FormAnswerEntry は FormAnswerContent と同じ条件でアクセス制御を行う
impl AuthorizationGuardWithContextDefinitions<AnswerEntry, AnswerEntryAuthorizationContext>
for AnswerEntry
{
fn can_create(&self, actor: &User, context: &AnswerEntryAuthorizationContext) -> bool {
let is_public_form = context.form_visibility == Visibility::PUBLIC;
let is_within_period = context.response_period.is_within_period(Utc::now());

(is_public_form && is_within_period) || actor.role == Role::Administrator
}

fn can_read(&self, actor: &User, context: &AnswerEntryAuthorizationContext) -> bool {
self.user().id == actor.id
|| context.answer_visibility == AnswerVisibility::PUBLIC
|| actor.role == Role::Administrator
}

fn can_update(&self, _actor: &User, _context: &AnswerEntryAuthorizationContext) -> bool {
false
}

fn can_delete(&self, actor: &User, _context: &AnswerEntryAuthorizationContext) -> bool {
actor.role == Role::Administrator
}
}

pub struct FormAnswerContentAuthorizationContext<'a, Action: Actions> {
pub answer_entry_authorization_context: &'a AnswerEntryAuthorizationContext,
pub answer_entry:
&'a AuthorizationGuardWithContext<AnswerEntry, Action, AnswerEntryAuthorizationContext>,
}

// NOTE: FormAnswerContent は FormAnswerEntry と同じ条件でアクセス制御を行う
impl<Action: Actions>
AuthorizationGuardWithContextDefinitions<
FormAnswerContent,
FormAnswerContentAuthorizationContext<'_, Action>,
> for FormAnswerContent
{
fn can_create(
&self,
actor: &User,
context: &FormAnswerContentAuthorizationContext<'_, Action>,
) -> bool {
context
.answer_entry
.can_create(actor, context.answer_entry_authorization_context)
}

fn can_read(
&self,
actor: &User,
context: &FormAnswerContentAuthorizationContext<'_, Action>,
) -> bool {
context
.answer_entry
.can_read(actor, context.answer_entry_authorization_context)
}

fn can_update(
&self,
actor: &User,
context: &FormAnswerContentAuthorizationContext<'_, Action>,
) -> bool {
context
.answer_entry
.can_update(actor, context.answer_entry_authorization_context)
}

fn can_delete(
&self,
actor: &User,
context: &FormAnswerContentAuthorizationContext<'_, Action>,
) -> bool {
context
.answer_entry
.can_delete(actor, context.answer_entry_authorization_context)
}
}
1 change: 1 addition & 0 deletions server/domain/src/form/answer/settings.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod models;
103 changes: 103 additions & 0 deletions server/domain/src/form/answer/settings/models.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
use chrono::{DateTime, Utc};
#[cfg(test)]
use common::test_utils::arbitrary_opt_date_time;
use derive_getters::Getters;
use deriving_via::DerivingVia;
use errors::domain::DomainError;
#[cfg(test)]
use proptest_derive::Arbitrary;
use serde::{Deserialize, Serialize};
use strum_macros::{Display, EnumString};
use types::non_empty_string::NonEmptyString;

#[cfg_attr(test, derive(Arbitrary))]
#[derive(Clone, DerivingVia, Default, Debug, PartialEq)]
#[deriving(From, Into, IntoInner, Serialize(via: Option::<NonEmptyString>), Deserialize(via: Option::<NonEmptyString>
))]
pub struct DefaultAnswerTitle(Option<NonEmptyString>);

impl DefaultAnswerTitle {
pub fn new(title: Option<NonEmptyString>) -> Self {
Self(title)
}
}

#[cfg_attr(test, derive(Arbitrary))]
#[derive(
Serialize, Deserialize, Debug, EnumString, Display, Copy, Clone, Default, PartialOrd, PartialEq,
)]
pub enum AnswerVisibility {
PUBLIC,
#[default]
PRIVATE,
}

impl TryFrom<String> for AnswerVisibility {
type Error = DomainError;

fn try_from(value: String) -> Result<Self, Self::Error> {
use std::str::FromStr;
Self::from_str(&value).map_err(Into::into)
}
}

#[cfg_attr(test, derive(Arbitrary))]
#[derive(Serialize, Deserialize, Getters, Clone, Default, Debug, PartialEq)]
pub struct ResponsePeriod {
#[cfg_attr(test, proptest(strategy = "arbitrary_opt_date_time()"))]
#[serde(default)]
start_at: Option<DateTime<Utc>>,
#[cfg_attr(test, proptest(strategy = "arbitrary_opt_date_time()"))]
#[serde(default)]
end_at: Option<DateTime<Utc>>,
}

impl ResponsePeriod {
pub fn try_new(
start_at: Option<DateTime<Utc>>,
end_at: Option<DateTime<Utc>>,
) -> Result<Self, DomainError> {
match (start_at, end_at) {
(Some(start_at), Some(end_at)) if start_at > end_at => {
Err(DomainError::InvalidResponsePeriod)
}
_ => Ok(Self { start_at, end_at }),
}
}

pub fn is_within_period(&self, now: DateTime<Utc>) -> bool {
if let Some(start_at) = self.start_at {
if start_at > now {
return false;
}
}
if let Some(end_at) = self.end_at {
if end_at < now {
return false;
}
}
true
}
}

#[cfg_attr(test, derive(Arbitrary))]
#[derive(Serialize, Deserialize, Getters, Default, Debug, PartialEq)]
pub struct AnswerSettings {
default_answer_title: DefaultAnswerTitle,
visibility: AnswerVisibility,
response_period: ResponsePeriod,
}

impl AnswerSettings {
pub fn new(
default_answer_title: DefaultAnswerTitle,
visibility: AnswerVisibility,
response_period: ResponsePeriod,
) -> Self {
Self {
default_answer_title,
visibility,
response_period,
}
}
}
2 changes: 2 additions & 0 deletions server/domain/src/form/comment.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pub mod models;
pub mod service;
Loading
Loading