141
crates/api-types/src/dimension.rs
Normal file
141
crates/api-types/src/dimension.rs
Normal 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")
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod dimension;
|
||||
pub mod errors;
|
||||
pub mod mappers;
|
||||
pub mod requests;
|
||||
|
||||
24
crates/api-types/src/mappers/api_token.rs
Normal file
24
crates/api-types/src/mappers/api_token.rs
Normal 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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
45
crates/api-types/src/mappers/correlation.rs
Normal file
45
crates/api-types/src/mappers/correlation.rs
Normal 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(),
|
||||
},
|
||||
}
|
||||
}
|
||||
33
crates/api-types/src/mappers/cycle.rs
Normal file
33
crates/api-types/src/mappers/cycle.rs
Normal 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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
68
crates/api-types/src/mappers/import_metrics.rs
Normal file
68
crates/api-types/src/mappers/import_metrics.rs
Normal 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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
49
crates/api-types/src/mappers/metric.rs
Normal file
49
crates/api-types/src/mappers/metric.rs
Normal 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}")))
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
12
crates/api-types/src/mappers/provider.rs
Normal file
12
crates/api-types/src/mappers/provider.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
17
crates/api-types/src/mappers/restore.rs
Normal file
17
crates/api-types/src/mappers/restore.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
5
crates/api-types/src/requests/api_token.rs
Normal file
5
crates/api-types/src/requests/api_token.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
#[derive(Debug, serde::Deserialize, utoipa::ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MintApiTokenRequest {
|
||||
pub name: String,
|
||||
}
|
||||
5
crates/api-types/src/requests/cycle.rs
Normal file
5
crates/api-types/src/requests/cycle.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
#[derive(Debug, serde::Deserialize, utoipa::ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SetPreferencesRequest {
|
||||
pub tracks_cycle: bool,
|
||||
}
|
||||
@@ -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)]
|
||||
|
||||
19
crates/api-types/src/requests/import.rs
Normal file
19
crates/api-types/src/requests/import.rs
Normal 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>,
|
||||
}
|
||||
19
crates/api-types/src/requests/metric.rs
Normal file
19
crates/api-types/src/requests/metric.rs
Normal 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,
|
||||
}
|
||||
@@ -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};
|
||||
|
||||
14
crates/api-types/src/requests/provider.rs
Normal file
14
crates/api-types/src/requests/provider.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
18
crates/api-types/src/responses/api_token.rs
Normal file
18
crates/api-types/src/responses/api_token.rs
Normal 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,
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>,
|
||||
}
|
||||
|
||||
37
crates/api-types/src/responses/correlation.rs
Normal file
37
crates/api-types/src/responses/correlation.rs
Normal 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,
|
||||
}
|
||||
21
crates/api-types/src/responses/cycle.rs
Normal file
21
crates/api-types/src/responses/cycle.rs
Normal 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,
|
||||
}
|
||||
@@ -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>,
|
||||
|
||||
30
crates/api-types/src/responses/import.rs
Normal file
30
crates/api-types/src/responses/import.rs
Normal 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>,
|
||||
}
|
||||
9
crates/api-types/src/responses/metric.rs
Normal file
9
crates/api-types/src/responses/metric.rs
Normal 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>,
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
8
crates/api-types/src/responses/provider.rs
Normal file
8
crates/api-types/src/responses/provider.rs
Normal 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>,
|
||||
}
|
||||
11
crates/api-types/src/responses/restore.rs
Normal file
11
crates/api-types/src/responses/restore.rs
Normal 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>,
|
||||
}
|
||||
Reference in New Issue
Block a user