changes
All checks were successful
CI / ci (push) Successful in 19m38s

This commit is contained in:
2026-08-26 20:55:30 +02:00
parent a557c183e9
commit 23d052278a
523 changed files with 24448 additions and 2005 deletions

View File

@@ -0,0 +1,141 @@
use domain::dimension::DimensionValue;
use domain::entry::Content;
use domain::location::Coordinates;
use domain::provider::ProviderName;
use domain::song::{RecordingId, Song};
use domain::weather::{Celsius, Condition, Weather};
use crate::errors::ApiValidationError;
#[derive(Debug, serde::Serialize, serde::Deserialize, utoipa::ToSchema)]
#[serde(
tag = "kind",
rename_all = "camelCase",
rename_all_fields = "camelCase"
)]
pub enum DimensionPayload {
Content {
text: String,
},
Activities {
ids: Vec<String>,
},
Photos {
ids: Vec<String>,
},
VoiceMemos {
ids: Vec<String>,
},
Location {
latitude: f64,
longitude: f64,
},
Song {
title: String,
artist: String,
#[serde(skip_serializing_if = "Option::is_none", default)]
album: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", default)]
recording_id: Option<String>,
},
Weather {
condition: String,
temperature: f64,
observed_by: String,
},
}
impl From<&DimensionValue> for DimensionPayload {
fn from(value: &DimensionValue) -> Self {
match value {
DimensionValue::Content(content) => Self::Content {
text: content.value().to_string(),
},
DimensionValue::Activities(ids) => Self::Activities {
ids: ids.iter().map(|id| id.value().to_string()).collect(),
},
DimensionValue::Photos(ids) => Self::Photos {
ids: ids.iter().map(|id| id.value().to_string()).collect(),
},
DimensionValue::VoiceMemos(ids) => Self::VoiceMemos {
ids: ids.iter().map(|id| id.value().to_string()).collect(),
},
DimensionValue::Location(coordinates) => Self::Location {
latitude: coordinates.latitude().value(),
longitude: coordinates.longitude().value(),
},
DimensionValue::Weather(weather) => Self::Weather {
condition: weather.condition().name().to_string(),
temperature: weather.temperature().value(),
observed_by: weather.observed_by().value().to_string(),
},
DimensionValue::Song(song) => Self::Song {
title: song.title().value().to_string(),
artist: song.artist().value().to_string(),
album: song.album().map(|album| album.value().to_string()),
recording_id: song.recording_id().map(|id| id.value().to_string()),
},
}
}
}
impl DimensionPayload {
pub fn is_empty(&self) -> bool {
match self {
Self::Content { text } => text.trim().is_empty(),
Self::Activities { ids } | Self::Photos { ids } | Self::VoiceMemos { ids } => {
ids.is_empty()
}
Self::Location { .. } | Self::Song { .. } | Self::Weather { .. } => false,
}
}
pub fn into_dimension(self) -> Result<DimensionValue, ApiValidationError> {
match self {
Self::Content { text } => Ok(DimensionValue::Content(Content::new(text)?)),
Self::Activities { ids } => Ok(DimensionValue::activities(all_ids(&ids)?)),
Self::Photos { ids } => Ok(DimensionValue::Photos(all_ids(&ids)?)),
Self::VoiceMemos { ids } => Ok(DimensionValue::VoiceMemos(all_ids(&ids)?)),
Self::Location {
latitude,
longitude,
} => Ok(DimensionValue::Location(Coordinates::new(
latitude, longitude,
)?)),
Self::Weather {
condition,
temperature,
observed_by,
} => {
let condition = Condition::from_name(&condition).ok_or_else(|| {
ApiValidationError::Invalid(format!("unknown weather condition: {condition}"))
})?;
Ok(DimensionValue::Weather(Weather::new(
condition,
Celsius::new(temperature)?,
ProviderName::new(observed_by)?,
)))
}
Self::Song {
title,
artist,
album,
recording_id,
} => {
let recording_id = recording_id.as_deref().map(RecordingId::new).transpose()?;
Ok(DimensionValue::Song(Song::new(
title,
artist,
album,
recording_id,
)?))
}
}
}
}
fn all_ids<T: From<uuid::Uuid>>(ids: &[String]) -> Result<Vec<T>, ApiValidationError> {
crate::mappers::shared::parse_uuids_as(ids, ids.len(), "identifiers")
}

View File

@@ -1,3 +1,4 @@
pub mod dimension;
pub mod errors;
pub mod mappers;
pub mod requests;

View File

@@ -0,0 +1,24 @@
use domain::api_token::{ApiToken, MintedApiToken};
use crate::responses::{ApiTokenResponse, MintedApiTokenResponse};
impl From<ApiToken> for ApiTokenResponse {
fn from(token: ApiToken) -> Self {
Self {
id: token.id().value().to_string(),
name: token.name().value().to_string(),
scope: token.scope().name().to_string(),
created_at: *token.created_at(),
last_used_at: token.last_used_at().copied(),
}
}
}
impl From<MintedApiToken> for MintedApiTokenResponse {
fn from(minted: MintedApiToken) -> Self {
Self {
secret: minted.secret().to_string(),
token: minted.token().clone().into(),
}
}
}

View File

@@ -5,7 +5,7 @@ use application::import::use_cases::import_entries::ImportResult;
use crate::errors::ApiValidationError;
use crate::requests::ReplaceActivityRequest;
use crate::responses::{CorrelationResponse, ImportResultResponse};
use crate::responses::ImportResultResponse;
impl ReplaceActivityRequest {
pub fn into_parts(
@@ -18,16 +18,6 @@ impl ReplaceActivityRequest {
}
}
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 {

View File

@@ -5,9 +5,11 @@ 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:?}")),
date: day.date.value(),
day_mood: day.day_mood.map(|mood| mood.value()),
mood: day.day_mood.map(|mood| mood.rounded().value()),
mood_label: day.day_mood.map(|mood| format!("{:?}", mood.rounded())),
cycle_day: day.cycle_day.map(|cycle| cycle.value()),
entries: day.entries.into_iter().map(EntryResponse::from).collect(),
}
}

View File

@@ -0,0 +1,45 @@
use application::correlation::use_cases::get_correlations::{CorrelationRow, Score};
use domain::correlation::CorrelationInput;
use crate::responses::{
AgreementResponse, CorrelationInputResponse, CorrelationRowResponse, StrategyScoreResponse,
};
impl From<CorrelationRow> for CorrelationRowResponse {
fn from(row: CorrelationRow) -> Self {
Self {
input: input_response(&row.input, row.label.clone()),
sample_size: row.sample_size,
agreement: AgreementResponse {
agreeing: row.agreement.agreeing(),
applicable: row.agreement.applicable(),
},
scores: row.scores.into_iter().map(Into::into).collect(),
}
}
}
impl From<Score> for StrategyScoreResponse {
fn from(score: Score) -> Self {
Self {
strategy: score.strategy.name().to_string(),
coefficient: score.coefficient.value(),
held_up: score.held_up,
}
}
}
fn input_response(input: &CorrelationInput, label: Option<String>) -> CorrelationInputResponse {
match input {
CorrelationInput::Metric(kind) => CorrelationInputResponse::Metric {
metric: kind.name().to_string(),
},
CorrelationInput::MoonPhase => CorrelationInputResponse::MoonPhase,
CorrelationInput::CycleProgress => CorrelationInputResponse::CycleProgress,
CorrelationInput::Temperature => CorrelationInputResponse::Temperature,
CorrelationInput::Activity(id) => CorrelationInputResponse::Activity {
activity_id: id.value().to_string(),
name: label.unwrap_or_default(),
},
}
}

View File

@@ -0,0 +1,33 @@
use application::cycle::use_cases::read_cycle::CycleView;
use domain::cycle::CyclePosition;
use domain::user::UserPreferences;
use crate::responses::{CyclePositionResponse, CycleViewResponse, PreferencesResponse};
impl From<CycleView> for CycleViewResponse {
fn from(view: CycleView) -> Self {
Self {
tracking: view.tracking,
starts: view.starts.iter().map(|date| date.to_string()).collect(),
today: view.today.map(Into::into),
usual_length: view.usual_length,
}
}
}
impl From<CyclePosition> for CyclePositionResponse {
fn from(position: CyclePosition) -> Self {
Self {
day: position.day().value(),
progress: position.progress(),
}
}
}
impl From<UserPreferences> for PreferencesResponse {
fn from(preferences: UserPreferences) -> Self {
Self {
tracks_cycle: preferences.tracks_cycle(),
}
}
}

View File

@@ -1,7 +1,8 @@
use chrono::Utc;
use config::EntryConfig;
use domain::entry::{Content, DateRange, Mood, MoodEntry, MoodEntryId};
use domain::dimension::{ComposedEntry, DimensionValue};
use domain::entry::{DateRange, Mood, MoodEntryId};
use domain::user::UserId;
use application::entry::commands::{CreateEntryCommand, UpdateEntryCommand};
@@ -9,11 +10,12 @@ use application::entry::queries::{
FilterByActivityQuery, FilterByMoodQuery, ListEntriesQuery, MoodStatsQuery,
};
use crate::dimension::DimensionPayload;
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};
use super::shared::{parse_datetime, validate_content_length};
impl CreateEntryRequest {
pub fn into_command(
@@ -26,31 +28,13 @@ impl CreateEntryRequest {
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",
)?;
let dimensions = parse_dimensions(self.dimensions, config)?;
Ok(CreateEntryCommand {
user_id,
mood,
logged_at,
activities,
content,
photos,
voice_memos,
dimensions,
})
}
}
@@ -62,35 +46,14 @@ impl UpdateEntryRequest {
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",
)?;
let logged_at = self.logged_at.as_deref().map(parse_datetime).transpose()?;
let dimensions = parse_dimensions(self.dimensions, config)?;
Ok(UpdateEntryCommand {
entry_id,
mood,
logged_at,
activities,
content,
photos,
voice_memos,
dimensions,
})
}
}
@@ -147,11 +110,9 @@ impl DateRangeParams {
}
}
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();
impl From<ComposedEntry> for EntryResponse {
fn from(composed: ComposedEntry) -> Self {
let entry = &composed.entry;
Self {
id: entry.id().value(),
@@ -159,33 +120,61 @@ impl From<MoodEntry> for EntryResponse {
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
photo_urls: composed
.photos()
.iter()
.map(|id| format!("/api/v1/media/photos/{id}"))
.map(|id| format!("/api/v1/media/photos/{}", id.value()))
.collect(),
photos: photo_ids,
voice_memo_urls: voice_memo_ids
voice_memo_urls: composed
.voice_memos()
.iter()
.map(|id| format!("/api/v1/media/voice-memos/{id}"))
.map(|id| format!("/api/v1/media/voice-memos/{}", id.value()))
.collect(),
voice_memos: voice_memo_ids,
dimensions: composed.dimensions.iter().map(Into::into).collect(),
created_at: *entry.created_at(),
updated_at: *entry.updated_at(),
}
}
}
fn parse_content(
content: Option<String>,
fn parse_dimensions(
payloads: Option<Vec<DimensionPayload>>,
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)
) -> Result<Vec<DimensionValue>, ApiValidationError> {
payloads
.unwrap_or_default()
.into_iter()
.filter(|payload| !payload.is_empty())
.map(|payload| {
within_limits(&payload, config)?;
payload.into_dimension()
})
.transpose()
.collect()
}
fn within_limits(
payload: &DimensionPayload,
config: &EntryConfig,
) -> Result<(), ApiValidationError> {
let (held, most, label) = match payload {
DimensionPayload::Content { text } => {
return validate_content_length(text, config.max_content_length);
}
DimensionPayload::Activities { ids } => {
(ids.len(), config.max_activities_per_entry, "activities")
}
DimensionPayload::Photos { ids } => (ids.len(), config.max_photos, "photos"),
DimensionPayload::VoiceMemos { ids } => (ids.len(), config.max_voice_memos, "voice memos"),
DimensionPayload::Location { .. }
| DimensionPayload::Song { .. }
| DimensionPayload::Weather { .. } => return Ok(()),
};
if held > most {
return Err(ApiValidationError::Invalid(format!(
"too many {label}, maximum is {most}"
)));
}
Ok(())
}

View File

@@ -0,0 +1,68 @@
use application::import::commands::{ImportedDay, ImportedMetric};
use application::import::use_cases::import_daily_metrics::{ImportOutcome, RejectionSummary};
use domain::rejection::RejectedMetric;
use crate::requests::{ImportDailyMetricsRequest, ImportedDayPayload, ImportedMetricPayload};
use crate::responses::{ImportOutcomeResponse, RejectedMetricResponse, RejectionResponse};
impl ImportDailyMetricsRequest {
pub fn into_days(self) -> Vec<ImportedDay> {
self.days.into_iter().map(Into::into).collect()
}
}
impl From<ImportedDayPayload> for ImportedDay {
fn from(payload: ImportedDayPayload) -> Self {
Self {
date: payload.date,
metrics: payload.metrics.into_iter().map(Into::into).collect(),
}
}
}
impl From<ImportedMetricPayload> for ImportedMetric {
fn from(payload: ImportedMetricPayload) -> Self {
Self {
kind: payload.kind,
value: payload.value,
}
}
}
impl From<ImportOutcome> for ImportOutcomeResponse {
fn from(outcome: ImportOutcome) -> Self {
Self {
accepted: outcome.accepted,
superseded: outcome.superseded,
rejected: outcome.rejected.into_iter().map(Into::into).collect(),
}
}
}
impl From<RejectionSummary> for RejectedMetricResponse {
fn from(summary: RejectionSummary) -> Self {
Self {
date: summary.date,
kind: summary.kind,
reason: summary.reason,
}
}
}
impl From<RejectedMetric> for RejectionResponse {
fn from(rejected: RejectedMetric) -> Self {
Self {
id: rejected.id().value().to_string(),
origin: rejected.origin().name().to_string(),
provider: rejected
.detail()
.provider()
.map(|name| name.value().to_string()),
date: rejected.detail().date().map(|date| date.to_string()),
kind: rejected.detail().kind().to_string(),
value: rejected.detail().value(),
reason: rejected.reason().to_string(),
recorded_at: *rejected.recorded_at(),
}
}
}

View File

@@ -0,0 +1,49 @@
use application::metric::commands::MetricChange;
use domain::entry::{Date, DateSpan};
use domain::metric::{DailyMetric, MetricKind, MetricValue};
use crate::errors::ApiValidationError;
use crate::requests::{DateSpanParams, MetricPayload};
use crate::responses::DailyMetricResponse;
impl From<DailyMetric> for DailyMetricResponse {
fn from(metric: DailyMetric) -> Self {
Self {
date: metric.date().to_string(),
kind: metric.kind().name().to_string(),
value: metric.value().count(),
provider: metric
.source()
.provider()
.map(|name| name.value().to_string()),
}
}
}
impl MetricPayload {
pub fn into_change(self) -> Result<MetricChange, ApiValidationError> {
let kind = MetricKind::from_name(&self.kind).ok_or_else(|| {
ApiValidationError::Invalid(format!("unknown metric kind: {}", self.kind))
})?;
match self.value {
None => Ok(MetricChange::Cleared(kind)),
Some(count) => Ok(MetricChange::Stated(MetricValue::of_kind(kind, count)?)),
}
}
}
impl DateSpanParams {
pub fn into_span(self) -> Result<DateSpan, ApiValidationError> {
Ok(DateSpan::new(
parse_date(&self.from)?,
parse_date(&self.to)?,
)?)
}
}
pub fn parse_date(date: &str) -> Result<Date, ApiValidationError> {
date.parse()
.map(Date::from_persistence)
.map_err(|_| ApiValidationError::Invalid(format!("invalid date: {date}")))
}

View File

@@ -1,11 +1,18 @@
mod activity;
mod api_token;
mod bulk;
mod calendar;
mod correlation;
mod cycle;
mod entry;
mod import_metrics;
mod media;
mod metric;
mod provider;
mod reminder;
mod restore;
pub mod shared;
mod stats;
mod user;
pub use bulk::correlation_response;
pub use metric::parse_date;

View File

@@ -0,0 +1,12 @@
use application::provider::use_cases::list_connections::ConnectionSummary;
use crate::responses::ProviderConnectionResponse;
impl From<ConnectionSummary> for ProviderConnectionResponse {
fn from(summary: ConnectionSummary) -> Self {
Self {
provider: summary.provider.value().to_string(),
connected_at: summary.connected_at,
}
}
}

View File

@@ -0,0 +1,17 @@
use application::restore::use_cases::restore_backup::RestoreOutcome;
use crate::responses::RestoreOutcomeResponse;
impl From<RestoreOutcome> for RestoreOutcomeResponse {
fn from(outcome: RestoreOutcome) -> Self {
Self {
entries: outcome.entries,
metrics: outcome.metrics,
cycle_starts: outcome.cycle_starts,
activities: outcome.activities,
reminders: outcome.reminders,
media: outcome.media,
unreadable: outcome.unreadable,
}
}
}

View File

@@ -0,0 +1,5 @@
#[derive(Debug, serde::Deserialize, utoipa::ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct MintApiTokenRequest {
pub name: String,
}

View File

@@ -0,0 +1,5 @@
#[derive(Debug, serde::Deserialize, utoipa::ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct SetPreferencesRequest {
pub tracks_cycle: bool,
}

View File

@@ -3,10 +3,7 @@
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>>,
pub dimensions: Option<Vec<crate::dimension::DimensionPayload>>,
}
#[derive(Debug, serde::Deserialize, utoipa::ToSchema)]
@@ -14,10 +11,7 @@ pub struct CreateEntryRequest {
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>>,
pub dimensions: Option<Vec<crate::dimension::DimensionPayload>>,
}
#[derive(Debug, serde::Deserialize, utoipa::ToSchema, utoipa::IntoParams)]

View File

@@ -0,0 +1,19 @@
#[derive(Debug, serde::Deserialize, utoipa::ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ImportDailyMetricsRequest {
pub days: Vec<ImportedDayPayload>,
}
#[derive(Debug, serde::Deserialize, utoipa::ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ImportedDayPayload {
pub date: String,
pub metrics: Vec<ImportedMetricPayload>,
}
#[derive(Debug, serde::Deserialize, utoipa::ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ImportedMetricPayload {
pub kind: String,
pub value: Option<i64>,
}

View File

@@ -0,0 +1,19 @@
#[derive(Debug, serde::Deserialize, utoipa::ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct SetDailyMetricsRequest {
pub metrics: Vec<MetricPayload>,
}
#[derive(Debug, serde::Deserialize, utoipa::ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct MetricPayload {
pub kind: String,
pub value: Option<i64>,
}
#[derive(Debug, serde::Deserialize, utoipa::ToSchema, utoipa::IntoParams)]
#[serde(rename_all = "camelCase")]
pub struct DateSpanParams {
pub from: String,
pub to: String,
}

View File

@@ -1,14 +1,24 @@
mod activity;
mod api_token;
mod cycle;
mod entry;
mod import;
mod metric;
mod provider;
mod push;
mod reminder;
mod user;
pub use activity::{CreateActivityRequest, RenameActivityRequest, SetCategoryRequest};
pub use api_token::MintApiTokenRequest;
pub use cycle::SetPreferencesRequest;
pub use entry::{
CreateEntryRequest, DateRangeParams, ListEntriesParams, ReplaceActivityRequest,
UpdateEntryRequest,
};
pub use import::{ImportDailyMetricsRequest, ImportedDayPayload, ImportedMetricPayload};
pub use metric::{DateSpanParams, MetricPayload, SetDailyMetricsRequest};
pub use provider::ConnectProviderRequest;
pub use push::{PushSubscribeRequest, PushUnsubscribeRequest};
pub use reminder::{CreateReminderRequest, UpdateReminderRequest};
pub use user::{ChangePasswordRequest, LoginRequest, RegisterRequest, UpdateProfileRequest};

View File

@@ -0,0 +1,14 @@
#[derive(serde::Deserialize, utoipa::ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ConnectProviderRequest {
#[schema(value_type = Object)]
pub credential: serde_json::Value,
}
impl std::fmt::Debug for ConnectProviderRequest {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ConnectProviderRequest")
.field("credential", &"<redacted>")
.finish()
}
}

View File

@@ -0,0 +1,18 @@
use chrono::{DateTime, Utc};
#[derive(Debug, serde::Serialize, utoipa::ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ApiTokenResponse {
pub id: String,
pub name: String,
pub scope: String,
pub created_at: DateTime<Utc>,
pub last_used_at: Option<DateTime<Utc>>,
}
#[derive(Debug, serde::Serialize, utoipa::ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct MintedApiTokenResponse {
pub token: ApiTokenResponse,
pub secret: String,
}

View File

@@ -14,13 +14,6 @@ pub struct ImportResultResponse {
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 {

View File

@@ -7,6 +7,8 @@ use super::EntryResponse;
pub struct CalendarDayResponse {
pub date: NaiveDate,
pub entries: Vec<EntryResponse>,
pub dominant_mood: Option<u8>,
pub dominant_mood_label: Option<String>,
pub day_mood: Option<f64>,
pub mood: Option<u8>,
pub mood_label: Option<String>,
pub cycle_day: Option<u16>,
}

View File

@@ -0,0 +1,37 @@
#[derive(Debug, serde::Serialize, utoipa::ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct CorrelationRowResponse {
pub input: CorrelationInputResponse,
pub sample_size: usize,
pub agreement: AgreementResponse,
pub scores: Vec<StrategyScoreResponse>,
}
#[derive(Debug, serde::Serialize, utoipa::ToSchema)]
#[serde(
tag = "kind",
rename_all = "camelCase",
rename_all_fields = "camelCase"
)]
pub enum CorrelationInputResponse {
Metric { metric: String },
MoonPhase,
CycleProgress,
Temperature,
Activity { activity_id: String, name: String },
}
#[derive(Debug, serde::Serialize, utoipa::ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct AgreementResponse {
pub agreeing: usize,
pub applicable: usize,
}
#[derive(Debug, serde::Serialize, utoipa::ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct StrategyScoreResponse {
pub strategy: String,
pub coefficient: f64,
pub held_up: bool,
}

View File

@@ -0,0 +1,21 @@
#[derive(Debug, serde::Serialize, utoipa::ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct CycleViewResponse {
pub tracking: bool,
pub starts: Vec<String>,
pub today: Option<CyclePositionResponse>,
pub usual_length: i64,
}
#[derive(Debug, serde::Serialize, utoipa::ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct CyclePositionResponse {
pub day: u16,
pub progress: f64,
}
#[derive(Debug, serde::Serialize, utoipa::ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct PreferencesResponse {
pub tracks_cycle: bool,
}

View File

@@ -9,11 +9,8 @@ pub struct EntryResponse {
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 dimensions: Vec<crate::dimension::DimensionPayload>,
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,30 @@
use chrono::{DateTime, Utc};
#[derive(Debug, serde::Serialize, utoipa::ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ImportOutcomeResponse {
pub accepted: usize,
pub superseded: usize,
pub rejected: Vec<RejectedMetricResponse>,
}
#[derive(Debug, serde::Serialize, utoipa::ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct RejectedMetricResponse {
pub date: Option<String>,
pub kind: String,
pub reason: String,
}
#[derive(Debug, serde::Serialize, utoipa::ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct RejectionResponse {
pub id: String,
pub origin: String,
pub provider: Option<String>,
pub date: Option<String>,
pub kind: String,
pub value: Option<i64>,
pub reason: String,
pub recorded_at: DateTime<Utc>,
}

View File

@@ -0,0 +1,9 @@
#[derive(Debug, serde::Serialize, utoipa::ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct DailyMetricResponse {
pub date: String,
pub kind: String,
pub value: i64,
#[serde(skip_serializing_if = "Option::is_none")]
pub provider: Option<String>,
}

View File

@@ -1,15 +1,31 @@
mod activity;
mod api_token;
mod bulk;
mod calendar;
mod correlation;
mod cycle;
mod entry;
mod import;
mod metric;
mod provider;
mod reminder;
mod restore;
mod stats;
mod user;
pub use activity::ActivityResponse;
pub use bulk::{BulkActionResponse, CorrelationResponse, ImportResultResponse, MediaIdResponse};
pub use api_token::{ApiTokenResponse, MintedApiTokenResponse};
pub use bulk::{BulkActionResponse, ImportResultResponse, MediaIdResponse};
pub use calendar::CalendarDayResponse;
pub use correlation::{
AgreementResponse, CorrelationInputResponse, CorrelationRowResponse, StrategyScoreResponse,
};
pub use cycle::{CyclePositionResponse, CycleViewResponse, PreferencesResponse};
pub use entry::EntryResponse;
pub use import::{ImportOutcomeResponse, RejectedMetricResponse, RejectionResponse};
pub use metric::DailyMetricResponse;
pub use provider::ProviderConnectionResponse;
pub use reminder::{DayScheduleResponse, ReminderResponse};
pub use restore::RestoreOutcomeResponse;
pub use stats::{MoodFrequency, MoodStatsResponse};
pub use user::UserResponse;

View File

@@ -0,0 +1,8 @@
use chrono::{DateTime, Utc};
#[derive(Debug, serde::Serialize, utoipa::ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ProviderConnectionResponse {
pub provider: String,
pub connected_at: DateTime<Utc>,
}

View File

@@ -0,0 +1,11 @@
#[derive(Debug, serde::Serialize, utoipa::ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct RestoreOutcomeResponse {
pub entries: usize,
pub metrics: usize,
pub cycle_starts: usize,
pub activities: usize,
pub reminders: usize,
pub media: usize,
pub unreadable: Vec<String>,
}