init
Some checks failed
CI / ci (push) Failing after 1m48s

This commit is contained in:
2026-08-25 23:24:36 +02:00
commit 95739892de
466 changed files with 33918 additions and 0 deletions

View 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

View 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),
}

View File

@@ -0,0 +1,4 @@
pub mod errors;
pub mod mappers;
pub mod requests;
pub mod responses;

View 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(),
}
}
}

View 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,
}
}
}

View 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(),
}
}
}

View 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()
}

View 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() }
}
}

View 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;

View 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()
}

View 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(())
}

View 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,
}
}
}

View 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(),
}
}
}

View 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>,
}

View 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,
}

View 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};

View 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,
}
}
}

View 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>,
}

View 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,
}

View 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>,
}

View 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,
}

View 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>,
}

View 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>,
}

View 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;

View 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>,
}

View 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,
}

View 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>,
}