aboutsummaryrefslogtreecommitdiff
path: root/src/report.rs
blob: 07305a12a8095bb18c4b9b33353893d51025e7f6 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
use std::{fs, io, path::Path};

use atrium_api::{app::bsky::richtext::facet::Main as Facet, types::string::Language};
use serde::{Serialize, de::DeserializeOwned};

pub(crate) struct Post {
    pub text: String,
    pub facets: Vec<Facet>,
    /// Images to upload before creating the post record.
    pub images: Vec<PostImage>,
    pub langs: Vec<Language>,
    pub tags: Vec<String>,
}

pub(crate) struct PostImage {
    pub data: Vec<u8>,
    pub alt: String,
}

pub(crate) trait Report: Sized {
    type State: Default + DeserializeOwned + Serialize;

    const STATE_FILE: &'static str;

    async fn generate(state: Self::State) -> Result<(Vec<Post>, Self::State), String>;

    async fn report() -> Result<(Vec<Post>, impl FnOnce() -> Result<(), String>), String> {
        let state_path = Path::new(Self::STATE_FILE);
        let state = load_state(state_path)?;
        let (posts, new_state) = Self::generate(state).await?;
        Ok((posts, move || save_state(state_path, &new_state)))
    }
}

fn load_state<T>(path: &Path) -> Result<T, String>
where
    T: Default + DeserializeOwned,
{
    match fs::read(path) {
        Ok(data) => serde_json::from_slice(&data)
            .map_err(|error| format!("failed to parse {}: {error}", path.display())),
        Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(T::default()),
        Err(error) => Err(format!("failed to read {}: {error}", path.display())),
    }
}

fn save_state<T>(path: &Path, state: &T) -> Result<(), String>
where
    T: Serialize,
{
    let data = serde_json::to_vec_pretty(state)
        .map_err(|error| format!("failed to serialize {}: {error}", path.display()))?;
    let temporary_path = path.with_extension("json.tmp");

    fs::write(&temporary_path, data)
        .map_err(|error| format!("failed to write {}: {error}", temporary_path.display()))?;
    fs::rename(&temporary_path, path)
        .map_err(|error| format!("failed to replace {}: {error}", path.display()))
}