shift_client/types/rewards/
rewards_page.rs

1use crate::util::extract_csrf_token;
2use once_cell::sync::Lazy;
3use scraper::{ElementRef, Html, Selector};
4
5static ALERT_NOTICE_SELECTOR: Lazy<Selector> =
6    Lazy::new(|| Selector::parse(".alert.notice p").expect("invalid ALERT_NOTICE_SELECTOR"));
7
8/// Error that may occur while parsing a [`RewardsPage`].
9#[derive(Debug, thiserror::Error)]
10pub enum FromHtmlError {
11    /// MissingCsrfToken
12    #[error("missing csrf token")]
13    MissingCsrfToken,
14
15    /// Invalid alert notice
16    #[error("invalid alert notice")]
17    InvalidAlertNotice(#[from] FromElementError),
18}
19
20/// The rewards page
21#[derive(Debug)]
22pub struct RewardsPage {
23    /// The csrf token
24    pub csrf_token: String,
25
26    /// An alert notice
27    pub alert_notice: Option<AlertNotice>,
28}
29
30impl RewardsPage {
31    /// Parse a [`RewardsPage`] from html
32    pub(crate) fn from_html(html: &Html) -> Result<Self, FromHtmlError> {
33        let csrf_token = extract_csrf_token(html)
34            .ok_or(FromHtmlError::MissingCsrfToken)?
35            .to_string();
36
37        let alert_notice = html
38            .select(&ALERT_NOTICE_SELECTOR)
39            .next()
40            .map(AlertNotice::from_element)
41            .transpose()?;
42
43        Ok(Self {
44            csrf_token,
45            alert_notice,
46        })
47    }
48}
49
50/// An error that may occur while parsing an AlertNotice
51#[derive(Debug, thiserror::Error)]
52pub enum FromElementError {
53    /// Missing text
54    #[error("missing text")]
55    MissingText,
56
57    #[error("unknown text '{0}'")]
58    UnknownText(String),
59}
60
61#[derive(Debug)]
62pub enum AlertNotice {
63    /// A shift code was already redeemed
64    ShiftCodeAlreadyRedeemed,
65
66    /// Launch a shift game to redeem codes
67    LaunchShiftGame,
68
69    /// Redeemed a shift code
70    ShiftCodeRedeemed,
71
72    /// Redeem failed
73    ShiftCodeRedeemFail,
74}
75
76impl AlertNotice {
77    /// Parse an alert notice from an element
78    fn from_element(el: ElementRef) -> Result<Self, FromElementError> {
79        let text = el.text().next().ok_or(FromElementError::MissingText)?;
80
81        match text {
82            "This SHiFT code has already been redeemed" => Ok(Self::ShiftCodeAlreadyRedeemed),
83            "To continue to redeem SHiFT codes, please launch a SHiFT-enabled title first!" => {
84                Ok(Self::LaunchShiftGame)
85            }
86            "Your code was successfully redeemed" => Ok(Self::ShiftCodeRedeemed),
87            "Failed to redeem your SHiFT code" => Ok(Self::ShiftCodeRedeemFail),
88            _ => Err(FromElementError::UnknownText(text.to_string())),
89        }
90    }
91}
92
93#[cfg(test)]
94mod test {
95    use super::*;
96
97    const SAMPLE_1: &str = include_str!("../../../test_data/rewards_page_1.html");
98
99    #[test]
100    fn sample_1() {
101        let html = Html::parse_document(SAMPLE_1);
102        let _page = RewardsPage::from_html(&html).unwrap();
103    }
104}