Skip to content

Commit 0a9ea34

Browse files
committed
Add checks for number of slices and time slice resolution
1 parent 87a125e commit 0a9ea34

File tree

2 files changed

+53
-695
lines changed

2 files changed

+53
-695
lines changed

apps/labrinth/src/routes/v3/analytics_get.rs

Lines changed: 53 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
use std::num::NonZeroU64;
1111

1212
use actix_web::{HttpRequest, web};
13-
use chrono::{DateTime, Utc};
13+
use chrono::{DateTime, TimeDelta, Utc};
1414
use futures::StreamExt;
1515
use rust_decimal::Decimal;
1616
use serde::{Deserialize, Serialize};
@@ -38,25 +38,40 @@ pub fn config(cfg: &mut web::ServiceConfig) {
3838

3939
// request
4040

41+
/// Requests analytics data, aggregating over all possible analytics sources
42+
/// like projects and affiliate codes, returning the data in a list of time
43+
/// slices.
4144
#[derive(Debug, Serialize, Deserialize)]
4245
struct GetRequest {
46+
/// What time range to return statistics for.
4347
time_range: TimeRange,
48+
/// What analytics metrics to return data for.
4449
return_metrics: ReturnMetrics,
45-
// filters: Filters,
4650
}
4751

52+
/// Time range for fetching analytics.
4853
#[derive(Debug, Serialize, Deserialize)]
4954
struct TimeRange {
55+
/// When to start including data.
5056
start: DateTime<Utc>,
57+
/// When to stop including data.
5158
end: DateTime<Utc>,
59+
/// Determines how many time slices between the start and end will be
60+
/// included, and how fine-grained those time slices will be.
5261
resolution: TimeRangeResolution,
5362
}
5463

64+
/// Determines how many time slices between the start and end will be
65+
/// included, and how fine-grained those time slices will be.
5566
#[derive(Debug, Serialize, Deserialize)]
5667
#[serde(rename_all = "snake_case")]
5768
pub enum TimeRangeResolution {
58-
Minutes(NonZeroU64),
69+
/// Use a set number of time slices, with the resolution being determined
70+
/// automatically.
5971
Slices(NonZeroU64),
72+
/// Each time slice will be a set number of minutes long, and the number of
73+
/// slices is determined automatically.
74+
Minutes(NonZeroU64),
6075
}
6176

6277
#[derive(Debug, Serialize, Deserialize)]
@@ -303,7 +318,7 @@ async fn get(
303318
session_queue: web::Data<AuthQueue>,
304319
clickhouse: web::Data<clickhouse::Client>,
305320
) -> Result<web::Json<GetResponse>, ApiError> {
306-
const MIN_RESOLUTION_MINUTES: u64 = 60;
321+
const MIN_RESOLUTION: TimeDelta = TimeDelta::minutes(60);
307322
const MAX_TIME_SLICES: usize = 1024;
308323

309324
let (scopes, user) = get_user_from_headers(
@@ -315,36 +330,51 @@ async fn get(
315330
)
316331
.await?;
317332

318-
let num_time_slices = match req.time_range.resolution {
319-
TimeRangeResolution::Slices(slices) => slices.get(),
320-
TimeRangeResolution::Minutes(resolution_minutes) => {
321-
if resolution_minutes.get() < MIN_RESOLUTION_MINUTES {
322-
return Err(ApiError::InvalidInput(format!(
323-
"resolution must be at least {} minutes",
324-
MIN_RESOLUTION_MINUTES
325-
)));
326-
}
333+
let full_time_range = req.time_range.end - req.time_range.start;
334+
if full_time_range < TimeDelta::zero() {
335+
return Err(ApiError::InvalidInput(
336+
"end date must be after start date".into(),
337+
));
338+
}
327339

328-
let range_minutes = u64::try_from(
329-
(req.time_range.end - req.time_range.start).num_minutes(),
330-
)
331-
.map_err(|_| {
340+
let (num_time_slices, resolution) = match req.time_range.resolution {
341+
TimeRangeResolution::Slices(slices) => {
342+
let slices = i32::try_from(slices.get()).map_err(|_| {
332343
ApiError::InvalidInput(
333-
"time range end must be after start".into(),
344+
"number of slices must fit into an `i32`".into(),
334345
)
335346
})?;
336-
337-
range_minutes / resolution_minutes
347+
(slices, full_time_range / slices)
348+
}
349+
TimeRangeResolution::Minutes(resolution_minutes) => {
350+
let resolution_minutes = i64::try_from(resolution_minutes.get())
351+
.map_err(|_| {
352+
ApiError::InvalidInput(
353+
"resolution must fit into a `u64`".into(),
354+
)
355+
})?;
356+
let resolution = TimeDelta::minutes(resolution_minutes);
357+
358+
let num_slices =
359+
full_time_range.as_seconds_f64() / resolution.as_seconds_f64();
360+
361+
(num_slices as i32, resolution)
338362
}
339363
};
340-
let num_time_slices = usize::try_from(num_time_slices)
341-
.expect("u64 should fit within a usize");
342364

365+
let num_time_slices =
366+
usize::try_from(num_time_slices).expect("should fit within a usize");
343367
if num_time_slices > MAX_TIME_SLICES {
344368
return Err(ApiError::InvalidInput(format!(
345-
"resolution is too fine or range is too large - maximum of {MAX_TIME_SLICES} time slices"
369+
"resolution is too fine or range is too large - maximum of {MAX_TIME_SLICES} time slices, was {num_time_slices}"
370+
)));
371+
}
372+
if resolution < MIN_RESOLUTION {
373+
return Err(ApiError::InvalidInput(format!(
374+
"resolution must be at least {MIN_RESOLUTION}, was {resolution}",
346375
)));
347376
}
377+
348378
let mut time_slices = vec![TimeSlice::default(); num_time_slices];
349379

350380
// TODO fetch from req

0 commit comments

Comments
 (0)