128 lines
4.9 KiB
Rust
128 lines
4.9 KiB
Rust
use axum::Json;
|
|
use axum::extract::{Path, Query, State};
|
|
use axum::http::StatusCode;
|
|
|
|
use api_types::mappers::parse_date;
|
|
use api_types::requests::{DateSpanParams, ImportDailyMetricsRequest, SetDailyMetricsRequest};
|
|
use api_types::responses::{DailyMetricResponse, ImportOutcomeResponse, RejectionResponse};
|
|
use application::import::commands::ImportDailyMetricsCommand;
|
|
use application::import::use_cases::import_daily_metrics;
|
|
use application::metric::commands::SetDailyMetricsCommand;
|
|
use application::metric::use_cases::{list_daily_metrics, set_daily_metrics};
|
|
|
|
use crate::errors::ApiError;
|
|
use crate::extractors::{AuthenticatedUser, ImportingProvider, MetricWriter};
|
|
use crate::state::AppState;
|
|
|
|
#[utoipa::path(get, path = "/api/v1/metrics", tag = "metrics", security(("bearer" = [])),
|
|
params(DateSpanParams),
|
|
responses((status = 200, body = Vec<DailyMetricResponse>))
|
|
)]
|
|
pub async fn handle_list(
|
|
State(state): State<AppState>,
|
|
AuthenticatedUser(user_id): AuthenticatedUser,
|
|
Query(params): Query<DateSpanParams>,
|
|
) -> Result<Json<Vec<DailyMetricResponse>>, ApiError> {
|
|
let span = params.into_span()?;
|
|
|
|
let deps = list_daily_metrics::Deps {
|
|
metrics: state.daily_metric_query,
|
|
};
|
|
|
|
let metrics = list_daily_metrics::execute(user_id, span, &deps).await?;
|
|
|
|
Ok(Json(metrics.into_iter().map(Into::into).collect()))
|
|
}
|
|
|
|
#[utoipa::path(put, path = "/api/v1/metrics/{date}", tag = "metrics", security(("bearer" = [])),
|
|
description = "States the given metrics for one day. A null value clears that kind instead, \
|
|
after which a later provider import may report it again. Every kind may appear \
|
|
only once per request.",
|
|
params(("date" = String, Path, description = "Calendar date, as YYYY-MM-DD")),
|
|
request_body = SetDailyMetricsRequest,
|
|
responses((status = 204))
|
|
)]
|
|
pub async fn handle_set(
|
|
State(state): State<AppState>,
|
|
writer: MetricWriter,
|
|
Path(date): Path<String>,
|
|
Json(body): Json<SetDailyMetricsRequest>,
|
|
) -> Result<StatusCode, ApiError> {
|
|
let date = parse_date(&date)?;
|
|
|
|
let changes = body
|
|
.metrics
|
|
.into_iter()
|
|
.map(|payload| payload.into_change())
|
|
.collect::<Result<Vec<_>, _>>()?;
|
|
|
|
let deps = set_daily_metrics::Deps {
|
|
metrics: state.daily_metric_command,
|
|
users: state.user_query,
|
|
};
|
|
|
|
set_daily_metrics::execute(
|
|
SetDailyMetricsCommand {
|
|
user_id: writer.user_id,
|
|
date,
|
|
changes,
|
|
source: writer.source,
|
|
},
|
|
&deps,
|
|
)
|
|
.await?;
|
|
|
|
Ok(StatusCode::NO_CONTENT)
|
|
}
|
|
|
|
#[utoipa::path(post, path = "/api/v1/metrics/import", tag = "metrics", security(("bearer" = [])),
|
|
description = "Accepts a batch of days from an automation, authenticated by an api token and \
|
|
nothing else. A payload carrying only some of the eight kinds is normal. Every \
|
|
reading is judged on its own: the valid ones are stored and the rest are \
|
|
rejected and written to a trace the account holder can read, so one bad value \
|
|
never costs a night of good data. Values are never clamped. A reading the user \
|
|
has stated by hand is reported as superseding the imported one, which is not a \
|
|
rejection. Only a payload carrying more days than the configured limit is \
|
|
refused outright.",
|
|
request_body = ImportDailyMetricsRequest,
|
|
responses((status = 200, body = ImportOutcomeResponse))
|
|
)]
|
|
pub async fn handle_import(
|
|
State(state): State<AppState>,
|
|
importer: ImportingProvider,
|
|
Json(body): Json<ImportDailyMetricsRequest>,
|
|
) -> Result<Json<ImportOutcomeResponse>, ApiError> {
|
|
let deps = import_daily_metrics::Deps {
|
|
metrics: state.daily_metric_command,
|
|
rejections: state.rejection_command,
|
|
};
|
|
|
|
let outcome = import_daily_metrics::execute(
|
|
ImportDailyMetricsCommand {
|
|
user_id: importer.user_id,
|
|
provider: importer.provider,
|
|
days: body.into_days(),
|
|
maximum_days: state.import_config.maximum_days_per_import,
|
|
},
|
|
&deps,
|
|
)
|
|
.await?;
|
|
|
|
Ok(Json(outcome.into()))
|
|
}
|
|
|
|
#[utoipa::path(get, path = "/api/v1/metrics/rejections", tag = "metrics", security(("bearer" = [])),
|
|
description = "Readings that could not be used, most recent first, whether they arrived \
|
|
broken from an importer or were stored by an older build and can no longer be \
|
|
read. Only the most recent are kept.",
|
|
responses((status = 200, body = Vec<RejectionResponse>))
|
|
)]
|
|
pub async fn handle_rejections(
|
|
State(state): State<AppState>,
|
|
AuthenticatedUser(user_id): AuthenticatedUser,
|
|
) -> Result<Json<Vec<RejectionResponse>>, ApiError> {
|
|
let rejections = state.rejection_query.find_recent_by_user(&user_id).await?;
|
|
|
|
Ok(Json(rejections.into_iter().map(Into::into).collect()))
|
|
}
|