leyline-http 0.1.1

HTTP client that mimics browsers on the wire
Documentation
mod assign;
mod build;
mod entity;
mod fieldset;
mod page;
mod scan;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[non_exhaustive]
pub enum FormMethod {
    #[default]
    Get,
    Post,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[non_exhaustive]
pub enum FormEnctype {
    #[default]
    UrlEncoded,
    Multipart,
    TextPlain,
}

impl FormEnctype {
    const ALL: [FormEnctype; 3] = [Self::UrlEncoded, Self::Multipart, Self::TextPlain];

    #[must_use]
    pub fn as_str(self) -> &'static str {
        match self {
            Self::UrlEncoded => "application/x-www-form-urlencoded",
            Self::Multipart => "multipart/form-data",
            Self::TextPlain => "text/plain",
        }
    }

    fn parse(value: &str) -> Self {
        let value = value.trim();
        Self::ALL
            .into_iter()
            .find(|enctype| enctype.as_str().eq_ignore_ascii_case(value))
            .unwrap_or_default()
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Default)]
#[non_exhaustive]
pub struct Form {
    action: String,
    method: FormMethod,
    enctype: FormEnctype,
    base: Option<String>,
    id: Option<String>,
    name: Option<String>,
    fields: Vec<(String, String)>,
    buttons: Vec<Button>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
struct Button {
    name: String,
    value: String,
    image: bool,
}

#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct Anchor {
    href: String,
    text: String,
    rel: Option<String>,
}

#[must_use]
pub fn meta(document: &str, name: &str) -> Option<String> {
    page::meta(document, name)
}

#[must_use]
pub fn links(document: &str) -> Vec<Anchor> {
    page::links(document)
}

impl Anchor {
    #[must_use]
    pub fn href(&self) -> &str {
        &self.href
    }

    #[must_use]
    pub fn text(&self) -> &str {
        &self.text
    }

    #[must_use]
    pub fn rel(&self) -> Option<&str> {
        self.rel.as_deref()
    }
}

#[must_use]
pub fn forms(document: &str) -> Vec<Form> {
    build::forms(document)
}

impl Form {
    #[must_use]
    pub fn find(document: &str, selector: &str) -> Option<Form> {
        forms(document).into_iter().find(|form| {
            form.id.as_deref() == Some(selector) || form.name.as_deref() == Some(selector)
        })
    }

    #[must_use]
    pub fn action(&self) -> &str {
        &self.action
    }

    #[must_use]
    pub fn method(&self) -> FormMethod {
        self.method
    }

    #[must_use]
    pub fn enctype(&self) -> FormEnctype {
        self.enctype
    }

    #[must_use]
    pub fn base(&self) -> Option<&str> {
        self.base.as_deref()
    }

    #[must_use]
    pub fn id(&self) -> Option<&str> {
        self.id.as_deref()
    }

    #[must_use]
    pub fn name(&self) -> Option<&str> {
        self.name.as_deref()
    }

    #[must_use]
    pub fn fields(&self) -> &[(String, String)] {
        &self.fields
    }

    #[must_use]
    pub fn field(&self, name: &str) -> Option<&str> {
        self.fields
            .iter()
            .find(|(key, _)| key == name)
            .map(|(_, value)| value.as_str())
    }

    pub fn set(&mut self, name: impl Into<String>, value: impl Into<String>) -> &mut Self {
        let name = name.into();
        let value = value.into();
        match self.fields.iter().position(|(key, _)| *key == name) {
            Some(first) => {
                self.fields[first].1 = value;
                let mut index = 0;
                self.fields.retain(|(key, _)| {
                    let keep = index <= first || *key != name;
                    index += 1;
                    keep
                });
            }
            None => self.fields.push((name, value)),
        }
        self
    }

    #[must_use]
    pub fn buttons(&self) -> Vec<&str> {
        self.buttons.iter().map(|b| b.name.as_str()).collect()
    }

    pub fn press(&mut self, button: &str) -> bool {
        let Some(found) = self.buttons.iter().find(|b| b.name == button).cloned() else {
            return false;
        };
        if found.image {
            self.fields
                .push((format!("{}.x", found.name), "0".to_owned()));
            self.fields
                .push((format!("{}.y", found.name), "0".to_owned()));
        } else {
            self.fields.push((found.name, found.value));
        }
        true
    }
}