walker_common/report/
stats.rs

1use std::cmp::max;
2use std::fs::File;
3use std::io::ErrorKind;
4use std::path::Path;
5use time::OffsetDateTime;
6
7#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
8pub struct ReportStatistics {
9    /// Timestamp of the report
10    #[serde(with = "time::serde::rfc3339")]
11    pub last_run: time::OffsetDateTime,
12
13    #[serde(default)]
14    pub entries: Vec<Record>,
15}
16
17#[derive(Debug, thiserror::Error)]
18pub enum Error {
19    #[error(transparent)]
20    Serde(#[from] serde_json::Error),
21    #[error(transparent)]
22    Io(#[from] std::io::Error),
23}
24
25impl ReportStatistics {
26    pub fn load(path: impl AsRef<Path>) -> Result<Self, Error> {
27        Ok(serde_json::from_reader(File::open(path)?)?)
28    }
29
30    pub fn store(&self, path: impl AsRef<Path>) -> Result<(), Error> {
31        Ok(serde_json::to_writer(File::create(path)?, self)?)
32    }
33}
34
35#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
36pub struct Record {
37    /// Timestamp of the report
38    #[serde(with = "time::serde::rfc3339")]
39    pub timestamp: time::OffsetDateTime,
40
41    /// The total number of documents
42    pub total: usize,
43
44    /// The number of documents with an error
45    pub errors: usize,
46    /// The total number of errors
47    pub total_errors: usize,
48    /// The number of documents with a warning
49    pub warnings: usize,
50    /// The total number of warnings
51    pub total_warnings: usize,
52}
53
54pub fn record(path: impl AsRef<Path>, record: Record) -> Result<(), Error> {
55    // load stats, create default if not found, fail otherwise
56
57    let mut stats = match ReportStatistics::load(&path) {
58        Ok(stats) => stats,
59        Err(Error::Io(err)) if err.kind() == ErrorKind::NotFound => ReportStatistics {
60            last_run: OffsetDateTime::now_utc(),
61            entries: vec![],
62        },
63        Err(err) => return Err(err),
64    };
65
66    // update last_run timestamp
67
68    stats.last_run = max(stats.last_run, record.timestamp);
69
70    // insert record at the correct position
71
72    let pos = stats
73        .entries
74        .binary_search_by_key(&record.timestamp, |entry| entry.timestamp)
75        .unwrap_or_else(|e| e);
76    stats.entries.insert(pos, record);
77
78    // store
79
80    stats.store(path)?;
81
82    // done
83
84    Ok(())
85}
86
87pub struct Statistics {
88    pub total: usize,
89    pub errors: usize,
90    pub total_errors: usize,
91    pub warnings: usize,
92    pub total_warnings: usize,
93}
94
95/// Update the stats file with a new record, having the timestamp of `now`.
96pub fn record_now(stats_file: Option<&Path>, stats: Statistics) -> Result<(), Error> {
97    if let Some(statistics) = &stats_file {
98        let Statistics {
99            total,
100            errors,
101            total_errors,
102            warnings,
103            total_warnings,
104        } = stats;
105
106        record(
107            statistics,
108            Record {
109                timestamp: OffsetDateTime::now_utc(),
110                total,
111                errors,
112                total_errors,
113                warnings,
114                total_warnings,
115            },
116        )?;
117    }
118
119    Ok(())
120}