use lazy_static::lazy_static;
use scraper::node::Element;
use std::collections::HashMap;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum Error {
#[error("Unnamed inputs are not supported!")]
UnnamedInputError {},
#[error("Html tag '{element_tag}' cannot be parsed to struct Input!")]
UnsupportedElementTagError {
element_tag: String,
},
#[error("Input tag with attribte 'type={attr_type}' cannot be parsed to struct Input!")]
UnsupportedInputTypeError {
attr_type: String,
},
#[error("Missing attribute '{attribute}' on html tag '{element_tag}'!")]
MissingAttributeError {
attribute: String,
element_tag: String,
},
}
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum InputType {
Button,
Checkbox,
Color,
Date,
DateTimeLocal,
Email,
Hidden,
Month,
Number,
Password,
Range,
Reset,
Search,
Submit,
Tel,
Text,
Time,
Url,
Week,
}
lazy_static! {
static ref MAPPINGS: HashMap<&'static str, InputType> = {
HashMap::from([
("button", InputType::Button),
("checkbox", InputType::Checkbox),
("color", InputType::Color),
("date", InputType::Date),
("datetime-local", InputType::DateTimeLocal),
("email", InputType::Email),
("hidden", InputType::Hidden),
("month", InputType::Month),
("number", InputType::Number),
("password", InputType::Password),
("range", InputType::Range),
("reset", InputType::Reset),
("search", InputType::Search),
("submit", InputType::Submit),
("tel", InputType::Tel),
("text", InputType::Text),
("time", InputType::Time),
("url", InputType::Url),
("week", InputType::Week),
])
};
}
#[derive(Debug)]
pub struct Input {
t: InputType,
name: String,
value: Option<String>,
attr: HashMap<String, String>,
}
impl Input {
pub const fn t(&self) -> InputType {
self.t
}
pub fn name(&self) -> &str {
self.name.as_str()
}
pub fn value(&self) -> Option<&str> {
self.value.as_deref()
}
pub fn set_value(&mut self, new_value: Option<String>) -> Option<String> {
let prev = self.value.take();
self.value = new_value;
prev
}
pub fn attr(&self, attr: &str) -> Option<&str> {
self.attr.get(attr).map(|s| s.as_str())
}
pub fn set_attr(&mut self, attr: &str, new_value: Option<String>) -> Option<String> {
let prev;
if let Some(new_value) = new_value {
prev = self.attr.remove(attr);
self.attr.insert(attr.to_owned(), new_value);
} else {
prev = self.attr.remove(attr)
}
prev
}
pub(crate) fn parse(element: &Element) -> Result<Self> {
let tag_name = element.name().to_lowercase();
match tag_name.as_str() {
"input" => Self::parse_input(element),
"button" => Self::parse_button(element),
_ => Err(Error::UnsupportedElementTagError {
element_tag: tag_name,
}),
}
}
fn parse_input(element: &Element) -> Result<Self> {
let t = element
.attr("type")
.ok_or_else(|| Error::MissingAttributeError {
attribute: "type".to_owned(),
element_tag: element.name().to_owned(),
})?;
let t = MAPPINGS
.get(t)
.ok_or_else(|| Error::UnsupportedInputTypeError {
attr_type: t.to_owned(),
})?
.to_owned();
Self::parse_element(element, t)
}
fn parse_button(element: &Element) -> Result<Self> {
let t = element.attr("type").unwrap_or("submit").to_lowercase();
let t = match t.as_str() {
"submit" => InputType::Submit,
"reset" => InputType::Reset,
"button" => InputType::Button,
_ => return Err(Error::UnsupportedInputTypeError { attr_type: t }),
};
Self::parse_element(element, t)
}
fn parse_element(element: &Element, t: InputType) -> Result<Self> {
let name = element
.attr("name")
.ok_or(Error::UnnamedInputError {})?
.to_owned();
let value = element.attr("value").map(|s| s.to_owned());
let mut attr = HashMap::new();
for (k, v) in element.attrs() {
attr.insert(k.to_owned(), v.to_owned());
}
Ok(Self {
t,
name,
value,
attr,
})
}
}
#[cfg(test)]
mod tests {
use super::{Input, InputType, Result};
use rstest::rstest;
use scraper::{Html, Selector};
#[rstest]
#[case("button", InputType::Button)]
#[case("checkbox", InputType::Checkbox)]
#[case("color", InputType::Color)]
#[case("date", InputType::Date)]
#[case("datetime-local", InputType::DateTimeLocal)]
#[case("email", InputType::Email)]
#[case("hidden", InputType::Hidden)]
#[case("month", InputType::Month)]
#[case("number", InputType::Number)]
#[case("password", InputType::Password)]
#[case("range", InputType::Range)]
#[case("reset", InputType::Reset)]
#[case("search", InputType::Search)]
#[case("submit", InputType::Submit)]
#[case("tel", InputType::Tel)]
#[case("text", InputType::Text)]
#[case("time", InputType::Time)]
#[case("url", InputType::Url)]
#[case("week", InputType::Week)]
fn parse_valid_inputs(
#[case] input_type: &str,
#[case] expected_type: InputType,
) -> Result<()> {
let raw_html = format!(
r#"<input class="the_class" name="the_{t}" type="{t}" value="the_value" k1="v1" k2="v2">"#,
t = input_type
);
let html = Html::parse_fragment(&raw_html);
let selector = Selector::parse("input").unwrap();
let element = html.select(&selector).next().unwrap();
let mut input = Input::parse(element.value())?;
assert_eq!(input.t(), expected_type);
assert_eq!(input.name(), format!("the_{input_type}"));
assert_eq!(input.value(), Some("the_value"));
assert_eq!(input.attr("k1"), Some("v1"));
assert_eq!(input.attr("k2"), Some("v2"));
assert_eq!(input.attr("k3"), None);
input.set_value(Some("new_value".to_owned()));
assert_eq!(input.value(), Some("new_value"));
input.set_value(None);
assert_eq!(input.value(), None);
input.set_attr("k1", Some("v1_new".to_owned()));
assert_eq!(input.attr("k1"), Some("v1_new"));
Ok(())
}
}