use std::{convert::Infallible, future::Future};
use url::Url;
pub use whatlang::Lang;
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct Document {
title: String,
body: String,
summary: Option<String>,
created_at: Option<chrono::DateTime<chrono::Utc>>,
updated_at: Option<chrono::DateTime<chrono::Utc>>,
}
impl Document {
pub async fn new<T: IntoDocument>(source: T) -> Result<Self, T::Error> {
source.into_document().await
}
pub fn language(&self) -> Option<whatlang::Lang> {
whatlang::detect_lang(&self.body)
}
pub fn from_parts(title: impl Into<String>, body: impl Into<String>) -> Self {
Self {
title: title.into(),
body: body.into(),
summary: None,
created_at: None,
updated_at: None,
}
}
pub fn set_summary(&mut self, summary: impl Into<String>) {
self.summary = Some(summary.into());
}
pub fn set_created_at(&mut self, created_at: chrono::DateTime<chrono::Utc>) {
self.created_at = Some(created_at);
}
pub fn set_updated_at(&mut self, updated_at: chrono::DateTime<chrono::Utc>) {
self.updated_at = Some(updated_at);
}
pub fn title(&self) -> &str {
&self.title
}
pub fn body(&self) -> &str {
&self.body
}
}
impl From<String> for Document {
fn from(value: String) -> Self {
Self::from_parts("", value)
}
}
impl From<&str> for Document {
fn from(value: &str) -> Self {
Self::from_parts("", value)
}
}
impl AsRef<Document> for Document {
fn as_ref(&self) -> &Document {
self
}
}
impl std::fmt::Display for Document {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}\n{}", self.title, self.body)
}
}
pub trait IntoDocument {
type Error: Send + Sync + 'static;
fn into_document(self) -> impl Future<Output = Result<Document, Self::Error>> + Send;
}
impl IntoDocument for String {
type Error = Infallible;
async fn into_document(self) -> Result<Document, Self::Error> {
Ok(Document::from_parts("", self))
}
}
impl IntoDocument for &String {
type Error = Infallible;
async fn into_document(self) -> Result<Document, Self::Error> {
Ok(Document::from_parts("", self.to_string()))
}
}
impl IntoDocument for &str {
type Error = Infallible;
async fn into_document(self) -> Result<Document, Self::Error> {
Ok(Document::from_parts("", self.to_string()))
}
}
impl IntoDocument for Document {
type Error = Infallible;
async fn into_document(self) -> Result<Document, Self::Error> {
Ok(self)
}
}
pub trait IntoDocuments {
type Error: Send + Sync + 'static;
fn into_documents(self) -> impl Future<Output = Result<Vec<Document>, Self::Error>> + Send;
}
impl<T: IntoDocument + Send + Sync, I> IntoDocuments for I
where
I: IntoIterator<Item = T> + Send + Sync,
<I as IntoIterator>::IntoIter: Send + Sync,
{
type Error = T::Error;
async fn into_documents(self) -> Result<Vec<Document>, Self::Error> {
let mut documents = Vec::new();
for document in self {
documents.push(document.into_document().await?);
}
Ok(documents)
}
}
#[derive(Debug, thiserror::Error)]
pub enum ExtractDocumentError {
#[error("Failed to fetch HTML: {0}")]
FetchHtml(#[from] reqwest::Error),
#[error("Failed to extract article: {0}")]
ExtractArticle(#[from] readability::error::Error),
#[error("Failed to parse URL: {0}")]
ParseUrl(#[from] url::ParseError),
}
pub(crate) async fn get_article(url: Url) -> Result<Document, ExtractDocumentError> {
let html = reqwest::get(url.clone()).await?.text().await?;
extract_article(&html)
}
pub(crate) fn extract_article(html: &str) -> Result<Document, ExtractDocumentError> {
let cleaned =
readability::extractor::extract(&mut html.as_bytes(), &Url::parse("https://example.com")?)
.unwrap();
Ok(Document::from_parts(cleaned.title, cleaned.text))
}
impl IntoDocument for Url {
type Error = ExtractDocumentError;
async fn into_document(self) -> Result<Document, Self::Error> {
get_article(self).await
}
}