elvish_core/
data.rs

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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
//! Getting data from advent of code, and data required to get data from advent of code (year and
//! session token).

use color_eyre::eyre;
use reqwest::blocking::Client;

/// Data for a day's puzzle.
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct Day {
    /// The puzzle's input, which is generated uniquely for each user.
    pub input: String,

    /// The description for part one of the puzzle
    pub description_1: String,

    /// Description for part two of the puzzle. It is `None` part 1 hasn't completed yet.
    pub description_2: Option<String>,
}

/// Gets the year from the environment.
pub fn get_env_year() -> eyre::Result<i16> {
    Ok(std::env::var("YEAR")?.parse()?)
}

/// Gets the year from the environment, or returns the current year
pub fn get_year() -> i16 {
    get_env_year().unwrap_or_else(|_| {
        tracing::warn!("No YEAR environment variable found, using current year");
        jiff::Zoned::now().year()
    })
}

/// Gets the session token from the environment
pub fn get_session_token() -> eyre::Result<String> {
    Ok(std::env::var("SESSION_TOKEN")?)
}

/// Get the day's [data](Day).
pub fn get(year: i16, day: u8, session_token: &str) -> eyre::Result<Day> {
    read_day(day).or_else(|_| {
        tracing::warn!("Day data not found in `.elvish`, fetching day...");
        eprintln!("Day data not found in `.elvish`, fetching day...");
        let data = fetch_day(year, day, session_token)?;
        let serialized = ron::to_string(&data)?;

        std::fs::create_dir_all(PARENT_PATH)?;
        std::fs::write(path(day), serialized)?;

        Ok(data)
    })
}

/// Fetches the data for a day from the advent of code website. Not cached. 
pub fn fetch_day(year: i16, day: u8, session_token: &str) -> eyre::Result<Day> {
    let client = reqwest::blocking::Client::new();
    let (desc1, desc2) = fetch_desc(&client, year, day, session_token)?;

    Ok(Day {
        input: fetch_day_input(&client, year, day, session_token)?,
        description_1: desc1,
        description_2: desc2,
    })
}

/// Fetches some url formatting the cookies to include the session token in order to be valid for
/// advent of code
fn fetch_aoc(client: &Client, url: &str, session_token: &str) -> eyre::Result<String> {
    let response = client
        .get(url)
        .header("Cookie", format!("session={session_token}"))
        .send()?
        .error_for_status()?
        .text()?;

    Ok(response)
}

/// Fetches the input for a day's puzzle
fn fetch_day_input(
    client: &Client,
    year: i16,
    day: u8,
    session_token: &str,
) -> eyre::Result<String> {
    let url = format!("https://adventofcode.com/{year}/day/{day}/input");

    fetch_aoc(client, &url, session_token)
}

/// Fetches the descriptions for a day's puzzle. Returns a tuple of part 1's description and
/// optionally part 2's description
fn fetch_desc(
    client: &Client,
    year: i16,
    day: u8,
    session_token: &str,
) -> eyre::Result<(String, Option<String>)> {
    let url = format!("https://adventofcode.com/{year}/day/{day}");

    let html = fetch_aoc(client, &url, session_token)?;
    let dom = tl::parse(&html, tl::ParserOptions::default())?;
    let parser = dom.parser();
    let elements = dom
        .query_selector(".day-desc")
        .expect("There should be at least one element with `day-desc` in AOC pages.");

    let mut descriptions = elements.map(|element| {
        let inner_html = element.get(parser).unwrap().inner_html(parser);
        let markdown = mdka::from_html(&inner_html);

        markdown
    });

    let desc1 = descriptions
        .next()
        .expect("AOC should have at least one day description marked with a `day-desc` class.");
    let desc2 = descriptions.next();

    Ok((desc1, desc2))
}

const PARENT_PATH: &str = ".elvish";
fn path(day: u8) -> impl AsRef<std::path::Path> {
    format!("{PARENT_PATH}/day{:02}.ron", day)
}

fn read_day(day: u8) -> eyre::Result<Day> {
    let day = std::fs::read_to_string(path(day))?;
    let day = ron::from_str(&day)?;

    Ok(day)
}