advent_of_code_data/client/
protocol.rs

1use thiserror::Error;
2
3use crate::{Answer, Day, Part, Year};
4
5// TODO: Use "service" rather than protocol.
6
7#[derive(Debug, Error)]
8pub enum ServiceError {
9    #[error("HTTP {}", .0)]
10    HttpStatusError(u16),
11    #[error("{}", .0)]
12    ReqwestError(#[from] reqwest::Error),
13}
14
15/// Abstraction of the communication protocol used to communicate with the
16/// Advent of Code backend web service enabling test mocks.
17pub trait ServiceConnector {
18    fn get_input(&self, day: Day, year: Year, session: &str) -> Result<String, ServiceError>;
19    fn submit_answer(
20        &self,
21        answer: &Answer,
22        part: Part,
23        day: Day,
24        year: Year,
25        session: &str,
26    ) -> Result<String, ServiceError>;
27}
28
29#[derive(Debug)]
30pub struct AdventOfCodeService {}
31
32impl AdventOfCodeService {
33    const ADVENT_OF_CODE_DOMAIN: &'static str = "adventofcode.com";
34    const ADVENT_OF_CODE_URL: &'static str = "https://adventofcode.com";
35
36    fn create_http_client(
37        &self,
38        session: Option<&str>,
39    ) -> Result<reqwest::blocking::Client, ServiceError> {
40        // Create an HTTP client for interacting with the Advent of Code website.
41        // TODO: verify dev@smacdo.com email OK
42        let cookies: reqwest::cookie::Jar = Default::default();
43
44        if let Some(session) = session {
45            let cookie_data = format!(
46                "session={}; Domain={}",
47                session,
48                Self::ADVENT_OF_CODE_DOMAIN
49            );
50
51            tracing::debug!("adding session id `{}` to cookie jar", cookie_data);
52
53            cookies.add_cookie_str(
54                &cookie_data,
55                &Self::ADVENT_OF_CODE_URL.parse::<reqwest::Url>().unwrap(),
56            );
57        }
58
59        Ok(reqwest::blocking::ClientBuilder::new()
60            .cookie_provider(cookies.into())
61            .user_agent("github.com/smacdo/advent-of-code-rust [email: dev@smacdo.com]")
62            .build()?)
63    }
64}
65
66impl ServiceConnector for AdventOfCodeService {
67    fn get_input(&self, day: Day, year: Year, session: &str) -> Result<String, ServiceError> {
68        let url = format!("{}/{}/day/{}/input", Self::ADVENT_OF_CODE_URL, year, day);
69
70        tracing::debug!(
71            "url to get puzzle input for day {} year {} is `{}`",
72            day,
73            year,
74            url
75        );
76
77        let response = self.create_http_client(Some(session))?.get(url).send()?;
78        tracing::debug!("server responed with HTTP {}", response.status());
79
80        if response.status() == reqwest::StatusCode::OK {
81            Ok(response.text().unwrap()) // TODO: Handle this!
82        } else {
83            Err(ServiceError::HttpStatusError(response.status().as_u16()))
84        }
85    }
86
87    fn submit_answer(
88        &self,
89        answer: &Answer,
90        part: Part,
91        day: Day,
92        year: Year,
93        session: &str,
94    ) -> Result<String, ServiceError> {
95        // TODO: Convert expects and unwraps into errors.
96        let url = format!("{}/{}/day/{}/answer", Self::ADVENT_OF_CODE_URL, year, day);
97
98        tracing::debug!(
99            "creating url to post puzzle answer for part {:?} day {} year {} answer `{}` with url = `{}`",
100            part,
101            day,
102            year,
103            answer,
104            url
105        );
106
107        let response = self
108            .create_http_client(Some(session))?
109            .post(url)
110            .form(&[
111                (
112                    "level",
113                    if part == Part::One {
114                        "1".to_string()
115                    } else {
116                        "2".to_string()
117                    },
118                ),
119                ("answer", answer.to_string()),
120            ])
121            .send()?;
122
123        tracing::debug!("server responed with HTTP {}", response.status());
124
125        if response.status() == reqwest::StatusCode::OK {
126            Ok(response.text().unwrap()) // TODO: Handle this!
127        } else {
128            Err(ServiceError::HttpStatusError(response.status().as_u16()))
129        }
130    }
131}
132
133impl AdventOfCodeService {}