use crate::fetch_availability::errors::{
FetchError, HeadlessChromeError, ParseError, ParseWhen2MeetError, ProcessResultError,
};
use crate::fetch_availability::model::{fold, Person, Slot};
use chrono::{DateTime, Utc};
use headless_chrome::{Browser, Tab};
use std::sync::Arc;
use url::Url;
pub fn parse_when2meet(url: &Url) -> Result<Vec<Slot>, ParseWhen2MeetError> {
let browser = match Browser::default() {
Ok(browser) => browser,
Err(_) => {
return Err(ParseWhen2MeetError::HeadlessChrome(
HeadlessChromeError::FailedToLaunch,
))
}
};
let tab = match browser.new_tab() {
Ok(tab) => tab,
Err(_) => {
return Err(ParseWhen2MeetError::HeadlessChrome(
HeadlessChromeError::FailedToNewTab,
))
}
};
match tab.navigate_to(url.as_str()) {
Ok(_) => {}
Err(_) => {
return Err(ParseWhen2MeetError::HeadlessChrome(
HeadlessChromeError::FailedToNavigate,
))
}
}
match tab.wait_until_navigated() {
Ok(_) => {}
Err(_) => {
return Err(ParseWhen2MeetError::HeadlessChrome(
HeadlessChromeError::FailedToWaitUntilNavigated,
))
}
}
let raw_names = match fetch_people_names(&tab) {
Ok(raw_names) => raw_names,
Err(fetch_error) => return Err(ParseWhen2MeetError::Fetch(fetch_error)),
};
let names = match parse_people_names_from_result(raw_names) {
Ok(names) => names,
Err(parse_error) => return Err(ParseWhen2MeetError::Parse(parse_error)),
};
let raw_avail_matrix = match fetch_avail_matrix(&tab) {
Ok(raw_avail_matrix) => raw_avail_matrix,
Err(fetch_error) => return Err(ParseWhen2MeetError::Fetch(fetch_error)),
};
let avail_matrix = match parse_avail_matrix_from_result(raw_avail_matrix) {
Ok(avail_matrix) => avail_matrix,
Err(parse_error) => return Err(ParseWhen2MeetError::Parse(parse_error)),
};
let slots = match process_names_and_matrix(names, avail_matrix) {
Ok(slots) => slots,
Err(process_result_error) => {
return Err(ParseWhen2MeetError::ProcessResult(process_result_error))
}
};
Ok(slots)
}
fn process_names_and_matrix(
names: Vec<Box<str>>,
avail_matrix: Vec<Box<str>>,
) -> Result<Vec<Slot>, ProcessResultError> {
let mut slots = Vec::new();
for section in avail_matrix {
let mut parts = section.split(',');
let start_timestamp_str = match parts.next() {
Some(timestamp) => timestamp,
None => return Err(ProcessResultError::AvailMatrixNoNext { section }),
};
let start_timestamp = match DateTime::parse_from_str(start_timestamp_str, "%s") {
Ok(timestamp) => timestamp.with_timezone(&Utc),
Err(_) => {
return Err(ProcessResultError::AvailMatrixFailedTimestampParse {
timestamp: start_timestamp_str.to_string(),
})
}
};
let people = names
.iter()
.zip(parts)
.map(|(name, available)| Person {
name: name.to_string().into_boxed_str(),
available: available == "1",
})
.collect();
slots.push(Slot::new(start_timestamp, people));
}
Ok(fold(slots))
}
fn fetch_people_names(tab: &Arc<Tab>) -> Result<String, FetchError> {
let js_func = r#"
(function () {
return PeopleNames.join(",")
})();
"#;
let names = match tab.evaluate(js_func, false) {
Ok(result) => result,
Err(_) => return Err(FetchError::FailedEval),
};
let raw_names = match names.value {
Some(value) => value.to_string(),
None => return Err(FetchError::EvalNoValue),
};
Ok(raw_names)
}
fn parse_people_names_from_result(raw_names: String) -> Result<Vec<Box<str>>, ParseError> {
if raw_names.len() <= 2 {
return Err(ParseError::EmptyRaw);
}
let names = &raw_names[1..raw_names.len() - 1];
Ok(names
.split(',')
.map(|x| x.to_string().into_boxed_str())
.collect())
}
fn fetch_avail_matrix(tab: &Arc<Tab>) -> Result<String, FetchError> {
let js_func = r#"
(function () {
return AvailableAtSlot.map((slotData, i) => {
return `${TimeOfSlot[i]},${PeopleIDs.map(id => slotData.includes(id) ? 1 : 0).join(",")}`;
}).join("|");
})();
"#;
let avail_matrix = match tab.evaluate(js_func, false) {
Ok(result) => result,
Err(_) => return Err(FetchError::FailedEval),
};
let raw_avail_matrix = match avail_matrix.value {
Some(value) => value.to_string(),
None => return Err(FetchError::EvalNoValue),
};
Ok(raw_avail_matrix)
}
fn parse_avail_matrix_from_result(raw_avail_matrix: String) -> Result<Vec<Box<str>>, ParseError> {
if raw_avail_matrix.len() <= 2 {
return Err(ParseError::EmptyRaw);
}
let avail_matrix = &raw_avail_matrix[1..raw_avail_matrix.len() - 1];
Ok(avail_matrix
.split('|')
.map(|x| x.to_string().into_boxed_str())
.collect())
}
#[cfg(test)]
mod tests {
use crate::fetch_availability::errors::{ParseError, ProcessResultError};
use crate::fetch_availability::model::{Person, Slot};
use crate::fetch_availability::parse::{
parse_avail_matrix_from_result, parse_people_names_from_result, process_names_and_matrix,
};
use chrono::{DateTime, Utc};
use claims::{assert_err, assert_ok};
#[test]
fn test_parse_avail_matrix_from_result_valid_str() {
let raw_avail_matrix = "'1693746000,0,0,0|1693746900,1,0,0|1693747800,0,1,0'".to_string();
let avail_matrix = parse_avail_matrix_from_result(raw_avail_matrix);
assert_ok!(&avail_matrix);
let avail_matrix = avail_matrix.unwrap();
assert!(avail_matrix.len() == 3);
assert!(avail_matrix[0] == "1693746000,0,0,0".to_string().into_boxed_str());
assert!(avail_matrix[1] == "1693746900,1,0,0".to_string().into_boxed_str());
assert!(avail_matrix[2] == "1693747800,0,1,0".to_string().into_boxed_str());
}
#[test]
fn test_parse_avail_matrix_from_result_invalid_str() {
let raw_avail_matrix = "''".to_string();
let avail_matrix = parse_avail_matrix_from_result(raw_avail_matrix);
assert_err!(&avail_matrix);
let avail_matrix = avail_matrix.unwrap_err();
assert!(matches!(avail_matrix, ParseError::EmptyRaw));
}
#[test]
fn test_parse_people_names_from_result_valid_str() {
let raw_names = "'Muneer,Brian,Garrett'".to_string();
let names = parse_people_names_from_result(raw_names);
assert_ok!(&names);
let names = names.unwrap();
assert!(names.len() == 3);
assert!(names[0] == "Muneer".to_string().into_boxed_str());
assert!(names[1] == "Brian".to_string().into_boxed_str());
assert!(names[2] == "Garrett".to_string().into_boxed_str());
}
#[test]
fn test_parse_people_names_from_result_invalid_str() {
let raw_names = "''".to_string();
let names = parse_people_names_from_result(raw_names);
assert_err!(&names);
let names = names.unwrap_err();
assert!(matches!(names, ParseError::EmptyRaw));
}
#[test]
fn test_process_names_and_matrix_valid() {
let names = vec![
"Muneer".to_string().into_boxed_str(),
"Brian".to_string().into_boxed_str(),
"Garrett".to_string().into_boxed_str(),
];
let avail_matrix = vec![
"1693746000,0,0,0".to_string().into_boxed_str(),
"1693746900,1,0,0".to_string().into_boxed_str(),
"1693747800,0,1,0".to_string().into_boxed_str(),
];
let slots = process_names_and_matrix(names, avail_matrix);
assert_ok!(&slots);
let slots = slots.unwrap();
assert!(slots.len() == 3);
assert!(
slots[0]
== Slot::new(
DateTime::parse_from_str("1693746000", "%s")
.unwrap()
.with_timezone(&Utc),
vec![
Person {
name: "Muneer".to_string().into_boxed_str(),
available: false
},
Person {
name: "Brian".to_string().into_boxed_str(),
available: false
},
Person {
name: "Garrett".to_string().into_boxed_str(),
available: false
}
]
)
);
assert!(
slots[1]
== Slot::new(
DateTime::parse_from_str("1693746900", "%s")
.unwrap()
.with_timezone(&Utc),
vec![
Person {
name: "Muneer".to_string().into_boxed_str(),
available: true
},
Person {
name: "Brian".to_string().into_boxed_str(),
available: false
},
Person {
name: "Garrett".to_string().into_boxed_str(),
available: false
}
]
)
);
assert!(
slots[2]
== Slot::new(
DateTime::parse_from_str("1693747800", "%s")
.unwrap()
.with_timezone(&Utc),
vec![
Person {
name: "Muneer".to_string().into_boxed_str(),
available: false
},
Person {
name: "Brian".to_string().into_boxed_str(),
available: true
},
Person {
name: "Garrett".to_string().into_boxed_str(),
available: false
}
]
)
);
}
#[test]
fn test_process_names_and_matrix_bad_timestamp() {
let names = vec![
"Muneer".to_string().into_boxed_str(),
"Brian".to_string().into_boxed_str(),
"Garrett".to_string().into_boxed_str(),
];
let avail_matrix = vec!["".to_string().into_boxed_str()];
let slots = process_names_and_matrix(names, avail_matrix);
assert_err!(&slots);
let slots = slots.unwrap_err();
assert_eq!(
slots,
ProcessResultError::AvailMatrixFailedTimestampParse {
timestamp: "".to_string(),
}
);
}
}