10
10
use std:: num:: NonZeroU64 ;
11
11
12
12
use actix_web:: { HttpRequest , web} ;
13
- use chrono:: { DateTime , Utc } ;
13
+ use chrono:: { DateTime , TimeDelta , Utc } ;
14
14
use futures:: StreamExt ;
15
15
use rust_decimal:: Decimal ;
16
16
use serde:: { Deserialize , Serialize } ;
@@ -38,25 +38,40 @@ pub fn config(cfg: &mut web::ServiceConfig) {
38
38
39
39
// request
40
40
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.
41
44
#[ derive( Debug , Serialize , Deserialize ) ]
42
45
struct GetRequest {
46
+ /// What time range to return statistics for.
43
47
time_range : TimeRange ,
48
+ /// What analytics metrics to return data for.
44
49
return_metrics : ReturnMetrics ,
45
- // filters: Filters,
46
50
}
47
51
52
+ /// Time range for fetching analytics.
48
53
#[ derive( Debug , Serialize , Deserialize ) ]
49
54
struct TimeRange {
55
+ /// When to start including data.
50
56
start : DateTime < Utc > ,
57
+ /// When to stop including data.
51
58
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.
52
61
resolution : TimeRangeResolution ,
53
62
}
54
63
64
+ /// Determines how many time slices between the start and end will be
65
+ /// included, and how fine-grained those time slices will be.
55
66
#[ derive( Debug , Serialize , Deserialize ) ]
56
67
#[ serde( rename_all = "snake_case" ) ]
57
68
pub enum TimeRangeResolution {
58
- Minutes ( NonZeroU64 ) ,
69
+ /// Use a set number of time slices, with the resolution being determined
70
+ /// automatically.
59
71
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 ) ,
60
75
}
61
76
62
77
#[ derive( Debug , Serialize , Deserialize ) ]
@@ -303,7 +318,7 @@ async fn get(
303
318
session_queue : web:: Data < AuthQueue > ,
304
319
clickhouse : web:: Data < clickhouse:: Client > ,
305
320
) -> Result < web:: Json < GetResponse > , ApiError > {
306
- const MIN_RESOLUTION_MINUTES : u64 = 60 ;
321
+ const MIN_RESOLUTION : TimeDelta = TimeDelta :: minutes ( 60 ) ;
307
322
const MAX_TIME_SLICES : usize = 1024 ;
308
323
309
324
let ( scopes, user) = get_user_from_headers (
@@ -315,36 +330,51 @@ async fn get(
315
330
)
316
331
. await ?;
317
332
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
+ }
327
339
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 ( |_| {
332
343
ApiError :: InvalidInput (
333
- "time range end must be after start " . into ( ) ,
344
+ "number of slices must fit into an `i32` " . into ( ) ,
334
345
)
335
346
} ) ?;
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)
338
362
}
339
363
} ;
340
- let num_time_slices = usize:: try_from ( num_time_slices)
341
- . expect ( "u64 should fit within a usize" ) ;
342
364
365
+ let num_time_slices =
366
+ usize:: try_from ( num_time_slices) . expect ( "should fit within a usize" ) ;
343
367
if num_time_slices > MAX_TIME_SLICES {
344
368
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}" ,
346
375
) ) ) ;
347
376
}
377
+
348
378
let mut time_slices = vec ! [ TimeSlice :: default ( ) ; num_time_slices] ;
349
379
350
380
// TODO fetch from req
0 commit comments