de_hypertext 0.2.0

serde_json ergonomics for parsing html
Documentation
#![allow(dead_code)]

/// Lets say you're trying to scrape a website that uses A/B testing.
/// Sometimes you get to see BookItems (A) with prices and ratings,
/// Sometimes you get to see BookItems (B) with short descriptions,
///
/// When an enum is provided, we'll try to complete the variants one by one until one of them
/// succeeds, in the order they were defined. So if you know that one variant is more common than
/// another, you should define it above.
use de_hypertext::DeserializeError;
use de_hypertext::Deserializer;
use std::error::Error;

#[derive(Debug)]
enum BooksPage {
    /// you can define enum variants with named fields
    A { items: Vec<BookItemA> },
    /// or enum variants with unnamed fields
    B(Vec<BookItemB>),
}

#[derive(Debug)]
struct BookItemA {}

#[derive(Debug)]
struct BookItemB {}

impl Deserializer for BooksPage {
    fn from_document(
        document: &de_hypertext::scraper::ElementRef,
    ) -> Result<Self, DeserializeError> {
        use de_hypertext::scraper::ElementRef;
        use de_hypertext::scraper::Selector;
        use de_hypertext::DeserializeError;
        use std::any::type_name;

        let a = |document: &ElementRef<'_>| -> Result<Self, DeserializeError> {
            let items = {
                document
                    .select(&Selector::parse("row > li").map_err(|e| {
                        DeserializeError::BuildingSelectorFailed {
                            struct_name: format!("{}::{}", type_name::<Self>().to_string(), "A"),
                            field: "items".to_string(),
                            selector: "row > li".to_string(),
                            reason: e.to_string(),
                        }
                    })?)
                    .map(|document| BookItemA::from_document(&document))
                    .collect::<Result<Vec<_>, _>>()?
            };
            Ok(Self::A { items })
        }(document);
        if a.is_ok() {
            return a;
        }

        |document: &ElementRef<'_>| -> Result<Self, DeserializeError> {
            let field_0 = {
                document
                    .select(&Selector::parse("row > li").map_err(|e| {
                        DeserializeError::BuildingSelectorFailed {
                            struct_name: format!("{}::{}", type_name::<Self>().to_string(), "B"),
                            field: "items".to_string(),
                            selector: "row > li".to_string(),
                            reason: e.to_string(),
                        }
                    })?)
                    .map(|document| BookItemB::from_document(&document))
                    .collect::<Result<Vec<_>, _>>()?
            };
            Ok(Self::B(field_0))
        }(document)
    }
}

impl Deserializer for BookItemB {
    fn from_document(
        _document: &de_hypertext::scraper::ElementRef,
    ) -> Result<Self, DeserializeError> {
        Ok(Self {})
    }
}

impl Deserializer for BookItemA {
    fn from_document(
        _document: &de_hypertext::scraper::ElementRef,
    ) -> Result<Self, DeserializeError> {
        Ok(Self {})
    }
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
    let html = r#"
        <html>
            <body>
            </body>
        </html>
    "#;
    let result = BooksPage::from_html(html)?;
    println!("{result:#?}");
    Ok(())
}