14
crates/api-types/Cargo.toml
Normal file
14
crates/api-types/Cargo.toml
Normal file
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "api-types"
|
||||
edition.workspace = true
|
||||
version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
domain.workspace = true
|
||||
application.workspace = true
|
||||
config.workspace = true
|
||||
serde.workspace = true
|
||||
thiserror.workspace = true
|
||||
uuid.workspace = true
|
||||
chrono.workspace = true
|
||||
utoipa.workspace = true
|
||||
10
crates/api-types/src/errors.rs
Normal file
10
crates/api-types/src/errors.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
use domain::errors::DomainError;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ApiValidationError {
|
||||
#[error(transparent)]
|
||||
Domain(#[from] DomainError),
|
||||
|
||||
#[error("{0}")]
|
||||
Invalid(String),
|
||||
}
|
||||
4
crates/api-types/src/lib.rs
Normal file
4
crates/api-types/src/lib.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
pub mod errors;
|
||||
pub mod mappers;
|
||||
pub mod requests;
|
||||
pub mod responses;
|
||||
66
crates/api-types/src/mappers/activity.rs
Normal file
66
crates/api-types/src/mappers/activity.rs
Normal file
@@ -0,0 +1,66 @@
|
||||
use domain::activity::{Activity, ActivityId, ActivityName, CategoryName};
|
||||
use domain::user::UserId;
|
||||
|
||||
use application::activity::commands::{
|
||||
CreateActivityCommand, RenameActivityCommand, SetCategoryCommand,
|
||||
};
|
||||
|
||||
use crate::errors::ApiValidationError;
|
||||
use crate::requests::{CreateActivityRequest, RenameActivityRequest, SetCategoryRequest};
|
||||
use crate::responses::ActivityResponse;
|
||||
|
||||
impl CreateActivityRequest {
|
||||
pub fn into_command(
|
||||
self,
|
||||
user_id: UserId,
|
||||
) -> Result<CreateActivityCommand, ApiValidationError> {
|
||||
let name = ActivityName::new(self.name)?;
|
||||
let category = self.category.map(CategoryName::new).transpose()?;
|
||||
|
||||
Ok(CreateActivityCommand {
|
||||
user_id,
|
||||
name,
|
||||
category,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl RenameActivityRequest {
|
||||
pub fn into_command(
|
||||
self,
|
||||
activity_id: ActivityId,
|
||||
) -> Result<RenameActivityCommand, ApiValidationError> {
|
||||
let new_name = ActivityName::new(self.new_name)?;
|
||||
|
||||
Ok(RenameActivityCommand {
|
||||
activity_id,
|
||||
new_name,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl SetCategoryRequest {
|
||||
pub fn into_command(
|
||||
self,
|
||||
activity_id: ActivityId,
|
||||
) -> Result<SetCategoryCommand, ApiValidationError> {
|
||||
let category = self.category.map(CategoryName::new).transpose()?;
|
||||
|
||||
Ok(SetCategoryCommand {
|
||||
activity_id,
|
||||
category,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Activity> for ActivityResponse {
|
||||
fn from(activity: Activity) -> Self {
|
||||
Self {
|
||||
id: activity.id().value(),
|
||||
name: activity.name().value().to_string(),
|
||||
category: activity.category().map(|c| c.value().to_string()),
|
||||
archived: activity.is_archived(),
|
||||
created_at: *activity.created_at(),
|
||||
}
|
||||
}
|
||||
}
|
||||
39
crates/api-types/src/mappers/bulk.rs
Normal file
39
crates/api-types/src/mappers/bulk.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
use domain::activity::ActivityId;
|
||||
use domain::user::UserId;
|
||||
|
||||
use application::import::use_cases::import_entries::ImportResult;
|
||||
|
||||
use crate::errors::ApiValidationError;
|
||||
use crate::requests::ReplaceActivityRequest;
|
||||
use crate::responses::{CorrelationResponse, ImportResultResponse};
|
||||
|
||||
impl ReplaceActivityRequest {
|
||||
pub fn into_parts(
|
||||
self,
|
||||
user_id: UserId,
|
||||
) -> Result<(UserId, ActivityId, ActivityId), ApiValidationError> {
|
||||
let old = super::shared::parse_uuid(&self.old_activity_id)?.into();
|
||||
let new = super::shared::parse_uuid(&self.new_activity_id)?.into();
|
||||
Ok((user_id, old, new))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn correlation_response(
|
||||
activity_id: ActivityId,
|
||||
correlation: Option<f64>,
|
||||
) -> CorrelationResponse {
|
||||
CorrelationResponse {
|
||||
activity_id: activity_id.value(),
|
||||
correlation,
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ImportResult> for ImportResultResponse {
|
||||
fn from(result: ImportResult) -> Self {
|
||||
Self {
|
||||
imported: result.imported,
|
||||
skipped: result.skipped,
|
||||
errors: result.errors,
|
||||
}
|
||||
}
|
||||
}
|
||||
14
crates/api-types/src/mappers/calendar.rs
Normal file
14
crates/api-types/src/mappers/calendar.rs
Normal file
@@ -0,0 +1,14 @@
|
||||
use application::entry::use_cases::get_calendar::CalendarDay;
|
||||
|
||||
use crate::responses::{CalendarDayResponse, EntryResponse};
|
||||
|
||||
impl From<CalendarDay> for CalendarDayResponse {
|
||||
fn from(day: CalendarDay) -> Self {
|
||||
Self {
|
||||
date: day.date,
|
||||
dominant_mood: day.dominant_mood.map(|m| m.value()),
|
||||
dominant_mood_label: day.dominant_mood.map(|m| format!("{m:?}")),
|
||||
entries: day.entries.into_iter().map(EntryResponse::from).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
191
crates/api-types/src/mappers/entry.rs
Normal file
191
crates/api-types/src/mappers/entry.rs
Normal file
@@ -0,0 +1,191 @@
|
||||
use chrono::Utc;
|
||||
|
||||
use config::EntryConfig;
|
||||
use domain::entry::{Content, DateRange, Mood, MoodEntry, MoodEntryId};
|
||||
use domain::user::UserId;
|
||||
|
||||
use application::entry::commands::{CreateEntryCommand, UpdateEntryCommand};
|
||||
use application::entry::queries::{
|
||||
FilterByActivityQuery, FilterByMoodQuery, ListEntriesQuery, MoodStatsQuery,
|
||||
};
|
||||
|
||||
use crate::errors::ApiValidationError;
|
||||
use crate::requests::{CreateEntryRequest, DateRangeParams, ListEntriesParams, UpdateEntryRequest};
|
||||
use crate::responses::EntryResponse;
|
||||
|
||||
use super::shared::{parse_datetime, parse_uuids_as, validate_content_length};
|
||||
|
||||
impl CreateEntryRequest {
|
||||
pub fn into_command(
|
||||
self,
|
||||
user_id: UserId,
|
||||
config: &EntryConfig,
|
||||
) -> Result<CreateEntryCommand, ApiValidationError> {
|
||||
let mood = Mood::try_from(self.mood)?;
|
||||
let logged_at = match self.logged_at {
|
||||
Some(dt) => parse_datetime(&dt)?,
|
||||
None => Utc::now().fixed_offset(),
|
||||
};
|
||||
let content = parse_content(self.content, config)?;
|
||||
let activities = parse_uuids_as(
|
||||
&self.activity_ids.unwrap_or_default(),
|
||||
config.max_activities_per_entry,
|
||||
"activities",
|
||||
)?;
|
||||
let photos = parse_uuids_as(
|
||||
&self.photo_ids.unwrap_or_default(),
|
||||
config.max_photos,
|
||||
"photos",
|
||||
)?;
|
||||
let voice_memos = parse_uuids_as(
|
||||
&self.voice_memo_ids.unwrap_or_default(),
|
||||
config.max_voice_memos,
|
||||
"voice memos",
|
||||
)?;
|
||||
|
||||
Ok(CreateEntryCommand {
|
||||
user_id,
|
||||
mood,
|
||||
logged_at,
|
||||
activities,
|
||||
content,
|
||||
photos,
|
||||
voice_memos,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl UpdateEntryRequest {
|
||||
pub fn into_command(
|
||||
self,
|
||||
entry_id: MoodEntryId,
|
||||
config: &EntryConfig,
|
||||
) -> Result<UpdateEntryCommand, ApiValidationError> {
|
||||
let mood = Mood::try_from(self.mood)?;
|
||||
let logged_at = match self.logged_at {
|
||||
Some(dt) => parse_datetime(&dt)?,
|
||||
None => Utc::now().fixed_offset(),
|
||||
};
|
||||
let content = parse_content(self.content, config)?;
|
||||
let activities = parse_uuids_as(
|
||||
&self.activity_ids.unwrap_or_default(),
|
||||
config.max_activities_per_entry,
|
||||
"activities",
|
||||
)?;
|
||||
let photos = parse_uuids_as(
|
||||
&self.photo_ids.unwrap_or_default(),
|
||||
config.max_photos,
|
||||
"photos",
|
||||
)?;
|
||||
let voice_memos = parse_uuids_as(
|
||||
&self.voice_memo_ids.unwrap_or_default(),
|
||||
config.max_voice_memos,
|
||||
"voice memos",
|
||||
)?;
|
||||
|
||||
Ok(UpdateEntryCommand {
|
||||
entry_id,
|
||||
mood,
|
||||
logged_at,
|
||||
activities,
|
||||
content,
|
||||
photos,
|
||||
voice_memos,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ListEntriesParams {
|
||||
pub fn into_query(self, user_id: UserId) -> Result<ListEntriesQuery, ApiValidationError> {
|
||||
let range = match (self.from, self.to) {
|
||||
(Some(from), Some(to)) => Some(DateRange::new(
|
||||
parse_datetime(&from)?,
|
||||
parse_datetime(&to)?,
|
||||
)?),
|
||||
_ => None,
|
||||
};
|
||||
Ok(ListEntriesQuery {
|
||||
user_id,
|
||||
range,
|
||||
limit: self.limit,
|
||||
offset: self.offset,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl DateRangeParams {
|
||||
pub fn into_date_range(self) -> Result<DateRange, ApiValidationError> {
|
||||
let from = parse_datetime(&self.from)?;
|
||||
let to = parse_datetime(&self.to)?;
|
||||
Ok(DateRange::new(from, to)?)
|
||||
}
|
||||
|
||||
pub fn into_stats_query(self, user_id: UserId) -> Result<MoodStatsQuery, ApiValidationError> {
|
||||
let range = Some(self.into_date_range()?);
|
||||
Ok(MoodStatsQuery { user_id, range })
|
||||
}
|
||||
|
||||
pub fn into_mood_filter(
|
||||
self,
|
||||
user_id: UserId,
|
||||
mood: u8,
|
||||
) -> Result<FilterByMoodQuery, ApiValidationError> {
|
||||
let mood = Mood::try_from(mood)?;
|
||||
Ok(FilterByMoodQuery { user_id, mood })
|
||||
}
|
||||
|
||||
pub fn into_activity_filter(
|
||||
self,
|
||||
user_id: UserId,
|
||||
activity_id: &str,
|
||||
) -> Result<FilterByActivityQuery, ApiValidationError> {
|
||||
let activity_id = super::shared::parse_uuid(activity_id)?.into();
|
||||
Ok(FilterByActivityQuery {
|
||||
user_id,
|
||||
activity_id,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MoodEntry> for EntryResponse {
|
||||
fn from(entry: MoodEntry) -> Self {
|
||||
let photo_ids: Vec<uuid::Uuid> = entry.photos().iter().map(|p| p.value()).collect();
|
||||
let voice_memo_ids: Vec<uuid::Uuid> =
|
||||
entry.voice_memos().iter().map(|v| v.value()).collect();
|
||||
|
||||
Self {
|
||||
id: entry.id().value(),
|
||||
user_id: entry.user_id().value(),
|
||||
mood: entry.mood().value(),
|
||||
mood_label: format!("{:?}", entry.mood()),
|
||||
logged_at: *entry.logged_at(),
|
||||
activities: entry.activities().iter().map(|a| a.value()).collect(),
|
||||
content: entry.content().map(|c| c.value().to_string()),
|
||||
photo_urls: photo_ids
|
||||
.iter()
|
||||
.map(|id| format!("/api/v1/media/photos/{id}"))
|
||||
.collect(),
|
||||
photos: photo_ids,
|
||||
voice_memo_urls: voice_memo_ids
|
||||
.iter()
|
||||
.map(|id| format!("/api/v1/media/voice-memos/{id}"))
|
||||
.collect(),
|
||||
voice_memos: voice_memo_ids,
|
||||
created_at: *entry.created_at(),
|
||||
updated_at: *entry.updated_at(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_content(
|
||||
content: Option<String>,
|
||||
config: &EntryConfig,
|
||||
) -> Result<Option<Content>, ApiValidationError> {
|
||||
content
|
||||
.filter(|s| !s.trim().is_empty())
|
||||
.map(|text| {
|
||||
validate_content_length(&text, config.max_content_length)?;
|
||||
Content::new(text).map_err(ApiValidationError::from)
|
||||
})
|
||||
.transpose()
|
||||
}
|
||||
15
crates/api-types/src/mappers/media.rs
Normal file
15
crates/api-types/src/mappers/media.rs
Normal file
@@ -0,0 +1,15 @@
|
||||
use domain::attachment::{PhotoId, VoiceMemoId};
|
||||
|
||||
use crate::responses::MediaIdResponse;
|
||||
|
||||
impl From<PhotoId> for MediaIdResponse {
|
||||
fn from(id: PhotoId) -> Self {
|
||||
Self { id: id.value() }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<VoiceMemoId> for MediaIdResponse {
|
||||
fn from(id: VoiceMemoId) -> Self {
|
||||
Self { id: id.value() }
|
||||
}
|
||||
}
|
||||
11
crates/api-types/src/mappers/mod.rs
Normal file
11
crates/api-types/src/mappers/mod.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
mod activity;
|
||||
mod bulk;
|
||||
mod calendar;
|
||||
mod entry;
|
||||
mod media;
|
||||
mod reminder;
|
||||
pub mod shared;
|
||||
mod stats;
|
||||
mod user;
|
||||
|
||||
pub use bulk::correlation_response;
|
||||
96
crates/api-types/src/mappers/reminder.rs
Normal file
96
crates/api-types/src/mappers/reminder.rs
Normal file
@@ -0,0 +1,96 @@
|
||||
use chrono::{NaiveTime, Weekday};
|
||||
|
||||
use domain::reminder::{DaySchedule, Reminder, ReminderId};
|
||||
use domain::user::UserId;
|
||||
|
||||
use application::reminder::commands::{CreateReminderCommand, UpdateReminderCommand};
|
||||
|
||||
use crate::errors::ApiValidationError;
|
||||
use crate::requests::{CreateReminderRequest, UpdateReminderRequest};
|
||||
use crate::responses::{DayScheduleResponse, ReminderResponse};
|
||||
|
||||
impl CreateReminderRequest {
|
||||
pub fn into_command(
|
||||
self,
|
||||
user_id: UserId,
|
||||
) -> Result<CreateReminderCommand, ApiValidationError> {
|
||||
let schedule = self.into_schedule()?;
|
||||
|
||||
Ok(CreateReminderCommand { user_id, schedule })
|
||||
}
|
||||
|
||||
pub fn into_schedule(self) -> Result<DaySchedule, ApiValidationError> {
|
||||
let mut schedule = DaySchedule::new();
|
||||
|
||||
set_day(&mut schedule, Weekday::Mon, &self.monday)?;
|
||||
set_day(&mut schedule, Weekday::Tue, &self.tuesday)?;
|
||||
set_day(&mut schedule, Weekday::Wed, &self.wednesday)?;
|
||||
set_day(&mut schedule, Weekday::Thu, &self.thursday)?;
|
||||
set_day(&mut schedule, Weekday::Fri, &self.friday)?;
|
||||
set_day(&mut schedule, Weekday::Sat, &self.saturday)?;
|
||||
set_day(&mut schedule, Weekday::Sun, &self.sunday)?;
|
||||
|
||||
Ok(schedule)
|
||||
}
|
||||
}
|
||||
|
||||
impl UpdateReminderRequest {
|
||||
pub fn into_command(
|
||||
self,
|
||||
reminder_id: ReminderId,
|
||||
) -> Result<UpdateReminderCommand, ApiValidationError> {
|
||||
let schedule = self.schedule.map(|s| s.into_schedule()).transpose()?;
|
||||
|
||||
Ok(UpdateReminderCommand {
|
||||
reminder_id,
|
||||
schedule,
|
||||
enabled: self.enabled,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Reminder> for ReminderResponse {
|
||||
fn from(reminder: Reminder) -> Self {
|
||||
Self {
|
||||
id: reminder.id().value(),
|
||||
schedule: DayScheduleResponse::from(reminder.schedule()),
|
||||
enabled: reminder.is_enabled(),
|
||||
created_at: *reminder.created_at(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&DaySchedule> for DayScheduleResponse {
|
||||
fn from(schedule: &DaySchedule) -> Self {
|
||||
Self {
|
||||
monday: schedule.time_for(Weekday::Mon).map(format_time),
|
||||
tuesday: schedule.time_for(Weekday::Tue).map(format_time),
|
||||
wednesday: schedule.time_for(Weekday::Wed).map(format_time),
|
||||
thursday: schedule.time_for(Weekday::Thu).map(format_time),
|
||||
friday: schedule.time_for(Weekday::Fri).map(format_time),
|
||||
saturday: schedule.time_for(Weekday::Sat).map(format_time),
|
||||
sunday: schedule.time_for(Weekday::Sun).map(format_time),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn set_day(
|
||||
schedule: &mut DaySchedule,
|
||||
day: Weekday,
|
||||
time_str: &Option<String>,
|
||||
) -> Result<(), ApiValidationError> {
|
||||
if let Some(s) = time_str {
|
||||
let time = parse_time(s)?;
|
||||
schedule.set_time(day, Some(time));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_time(s: &str) -> Result<NaiveTime, ApiValidationError> {
|
||||
NaiveTime::parse_from_str(s, "%H:%M")
|
||||
.map_err(|e| ApiValidationError::Invalid(format!("invalid time format: {e}")))
|
||||
}
|
||||
|
||||
fn format_time(time: NaiveTime) -> String {
|
||||
time.format("%H:%M").to_string()
|
||||
}
|
||||
44
crates/api-types/src/mappers/shared.rs
Normal file
44
crates/api-types/src/mappers/shared.rs
Normal file
@@ -0,0 +1,44 @@
|
||||
use crate::errors::ApiValidationError;
|
||||
|
||||
pub fn parse_datetime(
|
||||
s: &str,
|
||||
) -> Result<chrono::DateTime<chrono::FixedOffset>, ApiValidationError> {
|
||||
chrono::DateTime::parse_from_rfc3339(s)
|
||||
.map_err(|e| ApiValidationError::Invalid(format!("invalid datetime: {e}")))
|
||||
}
|
||||
|
||||
pub fn parse_uuid(s: &str) -> Result<uuid::Uuid, ApiValidationError> {
|
||||
s.parse()
|
||||
.map_err(|_| ApiValidationError::Invalid(format!("invalid UUID: {s}")))
|
||||
}
|
||||
|
||||
pub fn parse_uuids_as<T>(
|
||||
ids: &[String],
|
||||
max: usize,
|
||||
label: &str,
|
||||
) -> Result<Vec<T>, ApiValidationError>
|
||||
where
|
||||
T: From<uuid::Uuid>,
|
||||
{
|
||||
if ids.len() > max {
|
||||
return Err(ApiValidationError::Invalid(format!(
|
||||
"too many {label}, maximum is {max}"
|
||||
)));
|
||||
}
|
||||
|
||||
ids.iter()
|
||||
.map(|s| {
|
||||
let uuid = parse_uuid(s)?;
|
||||
Ok(T::from(uuid))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn validate_content_length(text: &str, max: usize) -> Result<(), ApiValidationError> {
|
||||
if text.len() > max {
|
||||
return Err(ApiValidationError::Invalid(format!(
|
||||
"content exceeds maximum length of {max}"
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
22
crates/api-types/src/mappers/stats.rs
Normal file
22
crates/api-types/src/mappers/stats.rs
Normal file
@@ -0,0 +1,22 @@
|
||||
use application::entry::use_cases::get_mood_stats::MoodStats;
|
||||
|
||||
use crate::responses::{MoodFrequency, MoodStatsResponse};
|
||||
|
||||
impl From<MoodStats> for MoodStatsResponse {
|
||||
fn from(stats: MoodStats) -> Self {
|
||||
Self {
|
||||
average: stats.average,
|
||||
frequency: stats
|
||||
.frequency
|
||||
.into_iter()
|
||||
.map(|(mood, count)| MoodFrequency {
|
||||
mood: mood.value(),
|
||||
mood_label: format!("{mood:?}"),
|
||||
count,
|
||||
})
|
||||
.collect(),
|
||||
current_streak: stats.current_streak,
|
||||
total_entries: stats.total_entries,
|
||||
}
|
||||
}
|
||||
}
|
||||
69
crates/api-types/src/mappers/user.rs
Normal file
69
crates/api-types/src/mappers/user.rs
Normal file
@@ -0,0 +1,69 @@
|
||||
use domain::user::{DisplayName, Email, Timezone, User, UserId, Username};
|
||||
|
||||
use application::user::commands::{
|
||||
ChangePasswordCommand, LoginCommand, RegisterCommand, UpdateProfileCommand,
|
||||
};
|
||||
|
||||
use crate::errors::ApiValidationError;
|
||||
use crate::requests::{ChangePasswordRequest, LoginRequest, RegisterRequest, UpdateProfileRequest};
|
||||
use crate::responses::UserResponse;
|
||||
|
||||
impl RegisterRequest {
|
||||
pub fn into_command(self) -> Result<RegisterCommand, ApiValidationError> {
|
||||
let username = Username::new(self.username)?;
|
||||
let email = Email::new(self.email)?;
|
||||
|
||||
Ok(RegisterCommand {
|
||||
username,
|
||||
email,
|
||||
password: self.password,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl LoginRequest {
|
||||
pub fn into_command(self) -> LoginCommand {
|
||||
LoginCommand {
|
||||
identifier: self.identifier,
|
||||
password: self.password,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl UpdateProfileRequest {
|
||||
pub fn into_command(self, user_id: UserId) -> Result<UpdateProfileCommand, ApiValidationError> {
|
||||
let display_name = self.display_name.map(DisplayName::new).transpose()?;
|
||||
|
||||
let timezone = self.timezone.map(Timezone::new).transpose()?;
|
||||
|
||||
Ok(UpdateProfileCommand {
|
||||
user_id,
|
||||
display_name,
|
||||
timezone,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ChangePasswordRequest {
|
||||
pub fn into_command(self, user_id: UserId) -> ChangePasswordCommand {
|
||||
ChangePasswordCommand {
|
||||
user_id,
|
||||
current_password: self.current_password,
|
||||
new_password: self.new_password,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<User> for UserResponse {
|
||||
fn from(user: User) -> Self {
|
||||
Self {
|
||||
id: user.id().value(),
|
||||
username: user.username().value().to_string(),
|
||||
email: user.email().value().to_string(),
|
||||
display_name: user.display_name().map(|d| d.value().to_string()),
|
||||
timezone: user.timezone().map(|t| t.value().to_string()),
|
||||
role: format!("{:?}", user.role()),
|
||||
created_at: *user.created_at(),
|
||||
}
|
||||
}
|
||||
}
|
||||
18
crates/api-types/src/requests/activity.rs
Normal file
18
crates/api-types/src/requests/activity.rs
Normal file
@@ -0,0 +1,18 @@
|
||||
#[derive(Debug, serde::Deserialize, utoipa::ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreateActivityRequest {
|
||||
pub name: String,
|
||||
pub category: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize, utoipa::ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RenameActivityRequest {
|
||||
pub new_name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize, utoipa::ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SetCategoryRequest {
|
||||
pub category: Option<String>,
|
||||
}
|
||||
44
crates/api-types/src/requests/entry.rs
Normal file
44
crates/api-types/src/requests/entry.rs
Normal file
@@ -0,0 +1,44 @@
|
||||
#[derive(Debug, serde::Deserialize, utoipa::ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreateEntryRequest {
|
||||
pub mood: u8,
|
||||
pub logged_at: Option<String>,
|
||||
pub activity_ids: Option<Vec<String>>,
|
||||
pub content: Option<String>,
|
||||
pub photo_ids: Option<Vec<String>>,
|
||||
pub voice_memo_ids: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize, utoipa::ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UpdateEntryRequest {
|
||||
pub mood: u8,
|
||||
pub logged_at: Option<String>,
|
||||
pub activity_ids: Option<Vec<String>>,
|
||||
pub content: Option<String>,
|
||||
pub photo_ids: Option<Vec<String>>,
|
||||
pub voice_memo_ids: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize, utoipa::ToSchema, utoipa::IntoParams)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ListEntriesParams {
|
||||
pub from: Option<String>,
|
||||
pub to: Option<String>,
|
||||
pub limit: Option<i64>,
|
||||
pub offset: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize, utoipa::ToSchema, utoipa::IntoParams)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DateRangeParams {
|
||||
pub from: String,
|
||||
pub to: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize, utoipa::ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ReplaceActivityRequest {
|
||||
pub old_activity_id: String,
|
||||
pub new_activity_id: String,
|
||||
}
|
||||
14
crates/api-types/src/requests/mod.rs
Normal file
14
crates/api-types/src/requests/mod.rs
Normal file
@@ -0,0 +1,14 @@
|
||||
mod activity;
|
||||
mod entry;
|
||||
mod push;
|
||||
mod reminder;
|
||||
mod user;
|
||||
|
||||
pub use activity::{CreateActivityRequest, RenameActivityRequest, SetCategoryRequest};
|
||||
pub use entry::{
|
||||
CreateEntryRequest, DateRangeParams, ListEntriesParams, ReplaceActivityRequest,
|
||||
UpdateEntryRequest,
|
||||
};
|
||||
pub use push::{PushSubscribeRequest, PushUnsubscribeRequest};
|
||||
pub use reminder::{CreateReminderRequest, UpdateReminderRequest};
|
||||
pub use user::{ChangePasswordRequest, LoginRequest, RegisterRequest, UpdateProfileRequest};
|
||||
33
crates/api-types/src/requests/push.rs
Normal file
33
crates/api-types/src/requests/push.rs
Normal file
@@ -0,0 +1,33 @@
|
||||
use application::push::commands::{SubscribePushCommand, UnsubscribePushCommand};
|
||||
use domain::user::UserId;
|
||||
|
||||
#[derive(Debug, serde::Deserialize, utoipa::ToSchema)]
|
||||
pub struct PushSubscribeRequest {
|
||||
pub endpoint: String,
|
||||
pub p256dh: String,
|
||||
pub auth: String,
|
||||
}
|
||||
|
||||
impl PushSubscribeRequest {
|
||||
pub fn into_command(self, user_id: UserId) -> SubscribePushCommand {
|
||||
SubscribePushCommand {
|
||||
user_id,
|
||||
endpoint: self.endpoint,
|
||||
p256dh: self.p256dh,
|
||||
auth: self.auth,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize, utoipa::ToSchema)]
|
||||
pub struct PushUnsubscribeRequest {
|
||||
pub endpoint: String,
|
||||
}
|
||||
|
||||
impl PushUnsubscribeRequest {
|
||||
pub fn into_command(self) -> UnsubscribePushCommand {
|
||||
UnsubscribePushCommand {
|
||||
endpoint: self.endpoint,
|
||||
}
|
||||
}
|
||||
}
|
||||
18
crates/api-types/src/requests/reminder.rs
Normal file
18
crates/api-types/src/requests/reminder.rs
Normal file
@@ -0,0 +1,18 @@
|
||||
#[derive(Debug, serde::Deserialize, utoipa::ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreateReminderRequest {
|
||||
pub monday: Option<String>,
|
||||
pub tuesday: Option<String>,
|
||||
pub wednesday: Option<String>,
|
||||
pub thursday: Option<String>,
|
||||
pub friday: Option<String>,
|
||||
pub saturday: Option<String>,
|
||||
pub sunday: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize, utoipa::ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UpdateReminderRequest {
|
||||
pub schedule: Option<CreateReminderRequest>,
|
||||
pub enabled: Option<bool>,
|
||||
}
|
||||
28
crates/api-types/src/requests/user.rs
Normal file
28
crates/api-types/src/requests/user.rs
Normal file
@@ -0,0 +1,28 @@
|
||||
#[derive(Debug, serde::Deserialize, utoipa::ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RegisterRequest {
|
||||
pub username: String,
|
||||
pub email: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize, utoipa::ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct LoginRequest {
|
||||
pub identifier: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize, utoipa::ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UpdateProfileRequest {
|
||||
pub display_name: Option<String>,
|
||||
pub timezone: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize, utoipa::ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ChangePasswordRequest {
|
||||
pub current_password: String,
|
||||
pub new_password: String,
|
||||
}
|
||||
12
crates/api-types/src/responses/activity.rs
Normal file
12
crates/api-types/src/responses/activity.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, serde::Serialize, utoipa::ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ActivityResponse {
|
||||
pub id: Uuid,
|
||||
pub name: String,
|
||||
pub category: Option<String>,
|
||||
pub archived: bool,
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
28
crates/api-types/src/responses/bulk.rs
Normal file
28
crates/api-types/src/responses/bulk.rs
Normal file
@@ -0,0 +1,28 @@
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, serde::Serialize, utoipa::ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BulkActionResponse {
|
||||
pub affected_count: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize, utoipa::ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ImportResultResponse {
|
||||
pub imported: u64,
|
||||
pub skipped: u64,
|
||||
pub errors: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize, utoipa::ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CorrelationResponse {
|
||||
pub activity_id: Uuid,
|
||||
pub correlation: Option<f64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize, utoipa::ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MediaIdResponse {
|
||||
pub id: Uuid,
|
||||
}
|
||||
12
crates/api-types/src/responses/calendar.rs
Normal file
12
crates/api-types/src/responses/calendar.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
use chrono::NaiveDate;
|
||||
|
||||
use super::EntryResponse;
|
||||
|
||||
#[derive(Debug, serde::Serialize, utoipa::ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CalendarDayResponse {
|
||||
pub date: NaiveDate,
|
||||
pub entries: Vec<EntryResponse>,
|
||||
pub dominant_mood: Option<u8>,
|
||||
pub dominant_mood_label: Option<String>,
|
||||
}
|
||||
20
crates/api-types/src/responses/entry.rs
Normal file
20
crates/api-types/src/responses/entry.rs
Normal file
@@ -0,0 +1,20 @@
|
||||
use chrono::{DateTime, FixedOffset, Utc};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, serde::Serialize, utoipa::ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct EntryResponse {
|
||||
pub id: Uuid,
|
||||
pub user_id: Uuid,
|
||||
pub mood: u8,
|
||||
pub mood_label: String,
|
||||
pub logged_at: DateTime<FixedOffset>,
|
||||
pub activities: Vec<Uuid>,
|
||||
pub content: Option<String>,
|
||||
pub photos: Vec<Uuid>,
|
||||
pub photo_urls: Vec<String>,
|
||||
pub voice_memos: Vec<Uuid>,
|
||||
pub voice_memo_urls: Vec<String>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
15
crates/api-types/src/responses/mod.rs
Normal file
15
crates/api-types/src/responses/mod.rs
Normal file
@@ -0,0 +1,15 @@
|
||||
mod activity;
|
||||
mod bulk;
|
||||
mod calendar;
|
||||
mod entry;
|
||||
mod reminder;
|
||||
mod stats;
|
||||
mod user;
|
||||
|
||||
pub use activity::ActivityResponse;
|
||||
pub use bulk::{BulkActionResponse, CorrelationResponse, ImportResultResponse, MediaIdResponse};
|
||||
pub use calendar::CalendarDayResponse;
|
||||
pub use entry::EntryResponse;
|
||||
pub use reminder::{DayScheduleResponse, ReminderResponse};
|
||||
pub use stats::{MoodFrequency, MoodStatsResponse};
|
||||
pub use user::UserResponse;
|
||||
23
crates/api-types/src/responses/reminder.rs
Normal file
23
crates/api-types/src/responses/reminder.rs
Normal file
@@ -0,0 +1,23 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, serde::Serialize, utoipa::ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DayScheduleResponse {
|
||||
pub monday: Option<String>,
|
||||
pub tuesday: Option<String>,
|
||||
pub wednesday: Option<String>,
|
||||
pub thursday: Option<String>,
|
||||
pub friday: Option<String>,
|
||||
pub saturday: Option<String>,
|
||||
pub sunday: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize, utoipa::ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ReminderResponse {
|
||||
pub id: Uuid,
|
||||
pub schedule: DayScheduleResponse,
|
||||
pub enabled: bool,
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
16
crates/api-types/src/responses/stats.rs
Normal file
16
crates/api-types/src/responses/stats.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
#[derive(Debug, serde::Serialize, utoipa::ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MoodFrequency {
|
||||
pub mood: u8,
|
||||
pub mood_label: String,
|
||||
pub count: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize, utoipa::ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MoodStatsResponse {
|
||||
pub average: Option<f64>,
|
||||
pub frequency: Vec<MoodFrequency>,
|
||||
pub current_streak: usize,
|
||||
pub total_entries: usize,
|
||||
}
|
||||
14
crates/api-types/src/responses/user.rs
Normal file
14
crates/api-types/src/responses/user.rs
Normal file
@@ -0,0 +1,14 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, serde::Serialize, utoipa::ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UserResponse {
|
||||
pub id: Uuid,
|
||||
pub username: String,
|
||||
pub email: String,
|
||||
pub display_name: Option<String>,
|
||||
pub timezone: Option<String>,
|
||||
pub role: String,
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
Reference in New Issue
Block a user