bddap_aoc/
persist.rs

1//! Keeps track of session tokens, caches input files.
2
3use std::{collections::HashMap, str::FromStr};
4
5const APP_NAME: &str = "bddap-aoc";
6pub const DEFAULT_PROFILE: &str = "default";
7
8#[derive(serde::Deserialize, serde::Serialize, Clone)]
9struct ProfileSettings {
10    session_token: String,
11}
12
13#[derive(serde::Serialize, PartialEq, Eq, Hash)]
14struct FileSafeString(String);
15
16impl FromStr for FileSafeString {
17    type Err = &'static str;
18
19    fn from_str(name: &str) -> Result<Self, Self::Err> {
20        let err = "Profile name must be filename-safe";
21        if !name
22            .chars()
23            .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == ' ')
24        {
25            return Err(err);
26        }
27        if name.starts_with('-') || name.starts_with(' ') {
28            return Err(err);
29        }
30        Ok(Self(name.to_owned()))
31    }
32}
33
34impl<'de> serde::Deserialize<'de> for FileSafeString {
35    fn deserialize<D>(deserializer: D) -> Result<FileSafeString, D::Error>
36    where
37        D: serde::Deserializer<'de>,
38    {
39        let s = String::deserialize(deserializer)?;
40        s.parse().map_err(serde::de::Error::custom)
41    }
42}
43
44#[derive(serde::Deserialize, serde::Serialize, Default)]
45pub struct Config {
46    profile: HashMap<FileSafeString, ProfileSettings>,
47}
48
49impl Config {
50    pub fn load() -> anyhow::Result<Self> {
51        let config_path = config_file()?;
52
53        // Treat missing config file as empty config
54        if !config_path.exists() {
55            return Ok(Config::default());
56        }
57
58        let config = std::fs::read_to_string(config_path)?;
59        let config: Config = toml::from_str(&config)?;
60        Ok(config)
61    }
62
63    fn save(&self) -> anyhow::Result<()> {
64        let config_path = config_file()?;
65
66        // ensure the directory exists
67        if let Some(parent) = config_path.parent() {
68            std::fs::create_dir_all(parent)?;
69        }
70
71        let config = toml::to_string_pretty(self)?;
72        std::fs::write(config_path, config)?;
73        Ok(())
74    }
75
76    fn get_profile(&self, name: FileSafeString) -> Profile {
77        let settings = self.profile.get(&name).cloned();
78        Profile { name, settings }
79    }
80
81    pub fn get_default_profile(&self) -> Profile {
82        self.get_profile(DEFAULT_PROFILE.parse().unwrap())
83    }
84}
85
86fn config_file() -> anyhow::Result<std::path::PathBuf> {
87    let base = xdg::BaseDirectories::with_prefix(APP_NAME)?;
88    Ok(base.get_cache_home().join("config.toml"))
89}
90
91fn cache_dir() -> anyhow::Result<std::path::PathBuf> {
92    let base = xdg::BaseDirectories::with_prefix(APP_NAME)?;
93    Ok(base.get_cache_home())
94}
95
96/// Prompt the user for their session token and save it to the config file.
97///
98/// cargo run -- login
99/// Enter session cookie from https://adventofcode.com/ : <cookie>
100/// Session cookie has been saved to <path>/.config/aoc/config.toml
101pub fn login() -> Result<(), anyhow::Error> {
102    println!("{}", include_str!("how_to_find_session_token.txt"));
103    let session_token = rpassword::prompt_password("Enter session cookie: ")?;
104    let mut config = Config::load()?;
105    let default_profile = DEFAULT_PROFILE.parse().unwrap();
106    let profile = config
107        .profile
108        .entry(default_profile)
109        .or_insert_with(|| ProfileSettings {
110            session_token: "".to_string(),
111        });
112    profile.session_token = session_token;
113    config.save()?;
114    println!(
115        "Session cookie has been saved to {}",
116        config_file()?.display()
117    );
118    Ok(())
119}
120
121fn get_or_create_cache(
122    profile: &FileSafeString,
123    year: usize,
124    day: usize,
125) -> anyhow::Result<std::path::PathBuf> {
126    let profile_cache_dir = cache_dir()?.join("inputs").join(&profile.0);
127    std::fs::create_dir_all(&profile_cache_dir)?;
128    Ok(profile_cache_dir.join(format!("{}-{}.txt", year, day)))
129}
130
131pub struct Profile {
132    name: FileSafeString,
133    settings: Option<ProfileSettings>,
134}
135
136impl Profile {
137    pub fn set_cached(&self, year: usize, day: usize, input: &str) -> anyhow::Result<()> {
138        let location = get_or_create_cache(&self.name, year, day)?;
139        std::fs::write(location, input)?;
140        Ok(())
141    }
142
143    pub fn get_cached(&self, year: usize, day: usize) -> anyhow::Result<Option<String>> {
144        let location = get_or_create_cache(&self.name, year, day)?;
145        if !location.exists() {
146            return Ok(None);
147        }
148        let input = std::fs::read_to_string(location)?;
149        Ok(Some(input))
150    }
151
152    pub fn get_session_token(&self) -> Option<&str> {
153        let settings = self.settings.as_ref()?;
154        Some(&settings.session_token)
155    }
156}