diff --git a/server/domain/src/form.rs b/server/domain/src/form.rs index c446ac88..4db0f5c4 100644 --- a/server/domain/src/form.rs +++ b/server/domain/src/form.rs @@ -1 +1,5 @@ +pub mod answer; +pub mod comment; +pub mod message; pub mod models; +pub mod question; diff --git a/server/domain/src/form/answer.rs b/server/domain/src/form/answer.rs new file mode 100644 index 00000000..e072fd8b --- /dev/null +++ b/server/domain/src/form/answer.rs @@ -0,0 +1 @@ +pub mod models; diff --git a/server/domain/src/form/answer/models.rs b/server/domain/src/form/answer/models.rs new file mode 100644 index 00000000..cf929c00 --- /dev/null +++ b/server/domain/src/form/answer/models.rs @@ -0,0 +1,37 @@ +use chrono::{DateTime, Utc}; +#[cfg(test)] +use proptest_derive::Arbitrary; +use serde::{Deserialize, Serialize}; + +use crate::{ + form::{models::FormId, question::models::QuestionId}, + user::models::User, +}; + +pub type AnswerId = types::IntegerId; + +#[derive(Serialize, Deserialize, PartialEq, Debug)] +pub struct FormAnswer { + pub id: AnswerId, + pub user: User, + pub timestamp: DateTime, + pub form_id: FormId, + pub title: Option, +} + +#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)] +pub struct FormAnswerContent { + pub answer_id: AnswerId, + pub question_id: QuestionId, + pub answer: String, +} + +pub type AnswerLabelId = types::IntegerId; + +#[cfg_attr(test, derive(Arbitrary))] +#[derive(Serialize, Deserialize, Debug, PartialEq)] +pub struct AnswerLabel { + pub id: AnswerLabelId, + pub answer_id: AnswerId, + pub name: String, +} diff --git a/server/domain/src/form/comment.rs b/server/domain/src/form/comment.rs new file mode 100644 index 00000000..e072fd8b --- /dev/null +++ b/server/domain/src/form/comment.rs @@ -0,0 +1 @@ +pub mod models; diff --git a/server/domain/src/form/comment/models.rs b/server/domain/src/form/comment/models.rs new file mode 100644 index 00000000..5bd21d79 --- /dev/null +++ b/server/domain/src/form/comment/models.rs @@ -0,0 +1,15 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +use crate::{form::answer::models::AnswerId, user::models::User}; + +pub type CommentId = types::IntegerId; + +#[derive(Serialize, Deserialize, Debug, PartialEq)] +pub struct Comment { + pub answer_id: AnswerId, + pub comment_id: CommentId, + pub content: String, + pub timestamp: DateTime, + pub commented_by: User, +} diff --git a/server/domain/src/form/message.rs b/server/domain/src/form/message.rs new file mode 100644 index 00000000..e072fd8b --- /dev/null +++ b/server/domain/src/form/message.rs @@ -0,0 +1 @@ +pub mod models; diff --git a/server/domain/src/form/message/models.rs b/server/domain/src/form/message/models.rs new file mode 100644 index 00000000..264fb2ee --- /dev/null +++ b/server/domain/src/form/message/models.rs @@ -0,0 +1,372 @@ +use chrono::{DateTime, Utc}; +use derive_getters::Getters; +use errors::domain::DomainError; + +use crate::{ + form::answer::models::FormAnswer, + types::authorization_guard::AuthorizationGuardDefinitions, + user::models::{Role::Administrator, User}, +}; + +pub type MessageId = types::Id; + +#[derive(Getters, PartialEq, Debug)] +pub struct Message { + id: MessageId, + related_answer: FormAnswer, + sender: User, + body: String, + timestamp: DateTime, +} + +impl AuthorizationGuardDefinitions for Message { + /// [`Message`] の作成権限があるかどうかを判定します。 + /// + /// 作成権限は以下の条件のどちらかを満たしている場合に与えられます。 + /// - [`actor`] が [`Administrator`] である場合 + /// - [`actor`] が関連する回答の回答者である場合 + /// + /// # Examples + /// ``` + /// use domain::{ + /// form::{answer::models::FormAnswer, message::models::Message}, + /// types::authorization_guard::AuthorizationGuardDefinitions, + /// user::models::{Role, User}, + /// }; + /// use uuid::Uuid; + /// + /// let respondent = User { + /// name: "respondent".to_string(), + /// id: Uuid::new_v4(), + /// role: Role::StandardUser, + /// }; + /// + /// let related_answer = FormAnswer { + /// id: Default::default(), + /// user: respondent.to_owned(), + /// timestamp: Default::default(), + /// form_id: Default::default(), + /// title: Default::default(), + /// }; + /// + /// let message = Message::try_new( + /// related_answer, + /// respondent.to_owned(), + /// "test message".to_string(), + /// ) + /// .unwrap(); + /// + /// let administrator = User { + /// name: "administrator".to_string(), + /// id: Uuid::new_v4(), + /// role: Role::Administrator, + /// }; + /// + /// let unrelated_standard_user = User { + /// name: "unrelated_user".to_string(), + /// id: Uuid::new_v4(), + /// role: Role::StandardUser, + /// }; + /// + /// assert!(message.can_create(&respondent)); + /// assert!(message.can_create(&administrator)); + /// assert!(!message.can_create(&unrelated_standard_user)); + /// ``` + fn can_create(&self, actor: &User) -> bool { + actor.role == Administrator + || (actor.id == self.sender.id && self.related_answer.user.id == self.sender.id) + } + + /// [`Message`] の読み取り権限があるかどうかを判定します。 + /// + /// 読み取り権限は以下の条件のどちらかを満たしている場合に与えられます。 + /// - [`actor`] が [`Administrator`] である場合 + /// - [`actor`] が関連する回答の回答者である場合 + /// + /// # Examples + /// ``` + /// use domain::{ + /// form::{answer::models::FormAnswer, message::models::Message}, + /// types::authorization_guard::AuthorizationGuardDefinitions, + /// user::models::{Role, User}, + /// }; + /// use uuid::Uuid; + /// + /// let respondent = User { + /// name: "respondent".to_string(), + /// id: Uuid::new_v4(), + /// role: Role::StandardUser, + /// }; + /// + /// let related_answer = FormAnswer { + /// id: Default::default(), + /// user: respondent.to_owned(), + /// timestamp: Default::default(), + /// form_id: Default::default(), + /// title: Default::default(), + /// }; + /// + /// let message = Message::try_new( + /// related_answer, + /// respondent.to_owned(), + /// "test message".to_string(), + /// ) + /// .unwrap(); + /// + /// let administrator = User { + /// name: "administrator".to_string(), + /// id: Uuid::new_v4(), + /// role: Role::Administrator, + /// }; + /// + /// let unrelated_standard_user = User { + /// name: "unrelated_user".to_string(), + /// id: Uuid::new_v4(), + /// role: Role::StandardUser, + /// }; + /// + /// assert!(message.can_read(&respondent)); + /// assert!(message.can_read(&administrator)); + /// assert!(!message.can_read(&unrelated_standard_user)); + /// ``` + fn can_read(&self, actor: &User) -> bool { + actor.role == Administrator || self.related_answer.user.id == actor.id + } + + /// [`Message`] の更新権限があるかどうかを判定します。 + /// + /// 更新権限は以下の条件を満たしている場合に与えられます。 + /// - [`actor`] がメッセージの送信者の場合 + /// + /// [`actor`] が [`Administrator`] である場合に更新権限が与えられないのは、 + /// メッセージの送信者が意図しない更新が行われることを防ぐためです。 + /// + /// # Examples + /// ``` + /// use domain::{ + /// form::{answer::models::FormAnswer, message::models::Message}, + /// types::authorization_guard::AuthorizationGuardDefinitions, + /// user::models::{Role, User}, + /// }; + /// use uuid::Uuid; + /// + /// let respondent = User { + /// name: "respondent".to_string(), + /// id: Uuid::new_v4(), + /// role: Role::StandardUser, + /// }; + /// + /// let related_answer = FormAnswer { + /// id: Default::default(), + /// user: respondent.to_owned(), + /// timestamp: Default::default(), + /// form_id: Default::default(), + /// title: Default::default(), + /// }; + /// + /// let message = Message::try_new( + /// related_answer, + /// respondent.to_owned(), + /// "test message".to_string(), + /// ) + /// .unwrap(); + /// + /// let administrator = User { + /// name: "administrator".to_string(), + /// id: Uuid::new_v4(), + /// role: Role::Administrator, + /// }; + /// + /// let unrelated_standard_user = User { + /// name: "unrelated_user".to_string(), + /// id: Uuid::new_v4(), + /// role: Role::StandardUser, + /// }; + /// + /// assert!(message.can_update(&respondent)); + /// assert!(!message.can_update(&administrator)); + /// assert!(!message.can_update(&unrelated_standard_user)); + /// ``` + fn can_update(&self, actor: &User) -> bool { + self.sender.id == actor.id + } + + /// [`Message`] の削除権限があるかどうかを判定します。 + /// + /// 削除権限は以下の条件を満たしている場合に与えられます。 + /// - [`actor`] がメッセージの送信者の場合 + /// + /// [`actor`] が [`Administrator`] である場合に更新権限が与えられないのは、 + /// メッセージの送信者が意図しない削除(メッセージ内容の改変)が行われることを防ぐためです。 + /// + /// # Examples + /// ``` + /// use domain::{ + /// form::{answer::models::FormAnswer, message::models::Message}, + /// types::authorization_guard::AuthorizationGuardDefinitions, + /// user::models::{Role, User}, + /// }; + /// use uuid::Uuid; + /// + /// let respondent = User { + /// name: "respondent".to_string(), + /// id: Uuid::new_v4(), + /// role: Role::StandardUser, + /// }; + /// + /// let related_answer = FormAnswer { + /// id: Default::default(), + /// user: respondent.to_owned(), + /// timestamp: Default::default(), + /// form_id: Default::default(), + /// title: Default::default(), + /// }; + /// + /// let message = Message::try_new( + /// related_answer, + /// respondent.to_owned(), + /// "test message".to_string(), + /// ) + /// .unwrap(); + /// + /// let administrator = User { + /// name: "administrator".to_string(), + /// id: Uuid::new_v4(), + /// role: Role::Administrator, + /// }; + /// + /// let unrelated_standard_user = User { + /// name: "unrelated_user".to_string(), + /// id: Uuid::new_v4(), + /// role: Role::StandardUser, + /// }; + /// + /// assert!(message.can_delete(&respondent)); + /// assert!(!message.can_delete(&administrator)); + /// assert!(!message.can_delete(&unrelated_standard_user)); + /// ``` + fn can_delete(&self, actor: &User) -> bool { + self.sender.id == actor.id + } +} + +impl Message { + /// [`Message`] の生成を試みます。 + /// + /// 以下の場合に失敗します。 + /// - [`body`] が空文字列の場合 + /// + /// # Examples + /// ``` + /// use domain::{ + /// form::{answer::models::FormAnswer, message::models::Message}, + /// user::models::{Role, User}, + /// }; + /// use uuid::Uuid; + /// + /// let user = User { + /// name: "user".to_string(), + /// id: Uuid::new_v4(), + /// role: Role::StandardUser, + /// }; + /// + /// let related_answer = FormAnswer { + /// id: 1.into(), + /// user: user.to_owned(), + /// timestamp: Default::default(), + /// form_id: Default::default(), + /// title: Default::default(), + /// }; + /// + /// let success_message = + /// Message::try_new(related_answer, user.to_owned(), "test message".to_string()); + /// + /// let related_answer = FormAnswer { + /// id: 1.into(), + /// user: user.to_owned(), + /// timestamp: Default::default(), + /// form_id: Default::default(), + /// title: Default::default(), + /// }; + /// let message_with_empty_body = Message::try_new(related_answer, user, "".to_string()); + /// + /// assert!(success_message.is_ok()); + /// assert!(message_with_empty_body.is_err()); + /// ``` + pub fn try_new( + related_answer: FormAnswer, + sender: User, + body: String, + ) -> Result { + if body.is_empty() { + return Err(DomainError::EmptyMessageBody); + } + + Ok(Self { + id: MessageId::new(), + related_answer, + sender, + body, + timestamp: Utc::now(), + }) + } + + /// [`Message`] の各フィールドの値を受け取り、[`Message`] を生成します。 + /// + /// # Examples + /// ``` + /// use chrono::{DateTime, Utc}; + /// use domain::{ + /// form::{ + /// answer::models::FormAnswer, + /// message::models::{Message, MessageId}, + /// }, + /// user::models::{Role, User}, + /// }; + /// use uuid::Uuid; + /// + /// let user = User { + /// name: "user".to_string(), + /// id: Uuid::new_v4(), + /// role: Role::StandardUser, + /// }; + /// + /// let related_answer = FormAnswer { + /// id: 1.into(), + /// user: user.to_owned(), + /// timestamp: Utc::now(), + /// form_id: Default::default(), + /// title: Default::default(), + /// }; + /// + /// unsafe { + /// let message = Message::from_raw_parts( + /// MessageId::new(), + /// related_answer, + /// user, + /// "test message".to_string(), + /// Utc::now(), + /// ); + /// } + /// ``` + /// + /// # Safety + /// この関数は [`Message`] のバリデーションをスキップするため、 + /// データベースからすでにバリデーションされているデータを読み出すときなど、 + /// データの信頼性が保証されている場合にのみ使用してください。 + pub unsafe fn from_raw_parts( + id: MessageId, + related_answer: FormAnswer, + sender: User, + body: String, + timestamp: DateTime, + ) -> Self { + Self { + id, + related_answer, + sender, + body, + timestamp, + } + } +} diff --git a/server/domain/src/form/models.rs b/server/domain/src/form/models.rs index 159401f8..cf6400c9 100644 --- a/server/domain/src/form/models.rs +++ b/server/domain/src/form/models.rs @@ -1,6 +1,6 @@ use chrono::{DateTime, Utc}; #[cfg(test)] -use common::test_utils::{arbitrary_date_time, arbitrary_opt_date_time, arbitrary_with_size}; +use common::test_utils::{arbitrary_date_time, arbitrary_opt_date_time}; use derive_getters::Getters; use deriving_via::DerivingVia; use errors::domain::{DomainError, DomainError::EmptyValue}; @@ -434,124 +434,6 @@ impl AuthorizationGuardDefinitions
for Form { } } -pub type QuestionId = types::IntegerId; - -#[cfg_attr(test, derive(Arbitrary))] -#[derive(Serialize, Deserialize, Clone, Getters, Debug, PartialEq)] -pub struct Question { - #[serde(default)] - pub id: Option, - pub form_id: FormId, - pub title: String, - pub description: Option, - pub question_type: QuestionType, - #[cfg_attr(test, proptest(strategy = "arbitrary_with_size(1..100)"))] - #[serde(default)] - pub choices: Vec, - pub is_required: bool, -} - -impl Question { - pub fn new( - id: Option, - form_id: FormId, - title: String, - description: Option, - question_type: QuestionType, - choices: Vec, - is_required: bool, - ) -> Self { - Self { - id, - form_id, - title, - description, - question_type, - choices, - is_required, - } - } - - pub fn from_raw_parts( - id: Option, - form_id: FormId, - title: String, - description: Option, - question_type: QuestionType, - choices: Vec, - is_required: bool, - ) -> Self { - Self { - id, - form_id, - title, - description, - question_type, - choices, - is_required, - } - } -} - -#[cfg_attr(test, derive(Arbitrary))] -#[derive( - Debug, Serialize, Deserialize, Clone, Copy, EnumString, PartialOrd, PartialEq, Display, -)] -#[strum(ascii_case_insensitive)] -pub enum QuestionType { - TEXT, - SINGLE, - MULTIPLE, -} - -impl TryFrom for QuestionType { - type Error = DomainError; - - fn try_from(value: String) -> Result { - use std::str::FromStr; - Self::from_str(&value).map_err(Into::into) - } -} - -pub type AnswerId = types::IntegerId; - -#[derive(Serialize, Deserialize, PartialEq, Debug)] -pub struct FormAnswer { - pub id: AnswerId, - pub user: User, - pub timestamp: DateTime, - pub form_id: FormId, - pub title: Option, -} - -#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)] -pub struct FormAnswerContent { - pub answer_id: AnswerId, - pub question_id: QuestionId, - pub answer: String, -} - -pub type CommentId = types::IntegerId; - -#[derive(Serialize, Deserialize, Debug, PartialEq)] -pub struct Comment { - pub answer_id: AnswerId, - pub comment_id: CommentId, - pub content: String, - pub timestamp: DateTime, - pub commented_by: User, -} - -pub type AnswerLabelId = types::IntegerId; - -#[cfg_attr(test, derive(Arbitrary))] -#[derive(Serialize, Deserialize, Debug, PartialEq)] -pub struct AnswerLabel { - pub id: AnswerLabelId, - pub answer_id: AnswerId, - pub name: String, -} - pub type LabelId = types::IntegerId