#![forbid(unsafe_code)]
#![warn(missing_docs, clippy::missing_docs_in_private_items)]
use hard_xml::{XmlRead, XmlWrite};
use serde::{Deserialize, Serialize};
use thiserror::Error;
#[derive(Debug, Error)]
pub enum Error {
#[error("OPML body has no <outline> elements")]
BodyHasNoOutlines,
#[error("Failed to read file")]
IoError(#[from] std::io::Error),
#[error("Unsupported OPML version: {0:?}")]
UnsupportedVersion(String),
#[error("Failed to process XML file")]
XmlError(#[from] hard_xml::XmlError),
}
#[derive(
XmlWrite, XmlRead, PartialEq, Eq, Debug, Clone, Serialize, Deserialize,
)]
#[xml(tag = "opml")]
pub struct OPML {
#[xml(attr = "version")]
pub version: String,
#[xml(child = "head")]
pub head: Option<Head>,
#[xml(child = "body")]
pub body: Body,
}
impl OPML {
#[deprecated(note = "Use from_str instead", since = "1.1.0")]
pub fn new(xml: &str) -> Result<Self, Error> {
Self::from_str(xml).map_err(Into::into)
}
#[allow(clippy::should_implement_trait)]
pub fn from_str(xml: &str) -> Result<Self, Error> {
let opml = <OPML as XmlRead>::from_str(xml)?;
let valid_versions = vec!["1.0", "1.1", "2.0"];
if !valid_versions.contains(&opml.version.as_str()) {
return Err(Error::UnsupportedVersion(opml.version));
}
if opml.body.outlines.is_empty() {
return Err(Error::BodyHasNoOutlines);
}
Ok(opml)
}
pub fn from_reader<R>(reader: &mut R) -> Result<Self, Error>
where
R: std::io::Read,
{
let mut s = String::new();
reader.read_to_string(&mut s)?;
Self::from_str(&s).map_err(Into::into)
}
pub fn add_feed(&mut self, text: &str, url: &str) -> &mut Self {
self.body.outlines.push(Outline {
text: text.to_string(),
xml_url: Some(url.to_string()),
..Outline::default()
});
self
}
#[deprecated(note = "Use to_string instead", since = "1.1.0")]
pub fn to_xml(&self) -> Result<String, Error> {
self.to_string()
}
pub fn to_string(&self) -> Result<String, Error> {
Ok(XmlWrite::to_string(self)?)
}
pub fn to_writer<W>(&self, writer: &mut W) -> Result<(), Error>
where
W: std::io::Write,
{
let xml_string = self.to_string()?;
writer.write_all(xml_string.as_bytes())?;
Ok(())
}
}
impl Default for OPML {
fn default() -> Self {
OPML {
version: "2.0".to_string(),
head: Some(Head::default()),
body: Body::default(),
}
}
}
#[derive(
XmlWrite,
XmlRead,
PartialEq,
Eq,
Debug,
Clone,
Default,
Serialize,
Deserialize,
)]
#[xml(tag = "head")]
pub struct Head {
#[xml(flatten_text = "title")]
pub title: Option<String>,
#[xml(flatten_text = "dateCreated")]
pub date_created: Option<String>,
#[xml(flatten_text = "dateModified")]
pub date_modified: Option<String>,
#[xml(flatten_text = "ownerName")]
pub owner_name: Option<String>,
#[xml(flatten_text = "ownerEmail")]
pub owner_email: Option<String>,
#[xml(flatten_text = "ownerId")]
pub owner_id: Option<String>,
#[xml(flatten_text = "docs")]
pub docs: Option<String>,
#[xml(flatten_text = "expansionState")]
pub expansion_state: Option<String>,
#[xml(flatten_text = "vertScrollState")]
pub vert_scroll_state: Option<i32>,
#[xml(flatten_text = "windowTop")]
pub window_top: Option<i32>,
#[xml(flatten_text = "windowLeft")]
pub window_left: Option<i32>,
#[xml(flatten_text = "windowBottom")]
pub window_bottom: Option<i32>,
#[xml(flatten_text = "windowRight")]
pub window_right: Option<i32>,
}
#[derive(
XmlWrite,
XmlRead,
PartialEq,
Eq,
Debug,
Clone,
Default,
Serialize,
Deserialize,
)]
#[xml(tag = "body")]
pub struct Body {
#[xml(child = "outline")]
pub outlines: Vec<Outline>,
}
#[derive(
XmlWrite,
XmlRead,
PartialEq,
Eq,
Debug,
Clone,
Default,
Serialize,
Deserialize,
)]
#[xml(tag = "outline")]
pub struct Outline {
#[xml(default, attr = "text")]
pub text: String,
#[xml(attr = "type")]
pub r#type: Option<String>,
#[xml(attr = "isComment")]
pub is_comment: Option<bool>,
#[xml(attr = "isBreakpoint")]
pub is_breakpoint: Option<bool>,
#[xml(attr = "created")]
pub created: Option<String>,
#[xml(attr = "category")]
pub category: Option<String>,
#[xml(child = "outline")]
pub outlines: Vec<Outline>,
#[xml(attr = "xmlUrl")]
pub xml_url: Option<String>,
#[xml(attr = "description")]
pub description: Option<String>,
#[xml(attr = "htmlUrl")]
pub html_url: Option<String>,
#[xml(attr = "language")]
pub language: Option<String>,
#[xml(attr = "title")]
pub title: Option<String>,
#[xml(attr = "version")]
pub version: Option<String>,
#[xml(attr = "url")]
pub url: Option<String>,
}
impl Outline {
pub fn add_feed(&mut self, name: &str, url: &str) -> &mut Self {
self.outlines.push(Outline {
text: name.to_string(),
xml_url: Some(url.to_string()),
..Outline::default()
});
self
}
}