From e225d7344cef05f03676f6579415999478328ead Mon Sep 17 00:00:00 2001 From: HampusM Date: Sat, 25 Feb 2023 20:36:06 +0100 Subject: feat: add variable list support --- src/description.rs | 89 +++++++++++++---- src/lib.rs | 1 + src/variable_list.rs | 264 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/xml/element.rs | 11 +++ 4 files changed, 348 insertions(+), 17 deletions(-) create mode 100644 src/variable_list.rs diff --git a/src/description.rs b/src/description.rs index adc5324..dfa521b 100644 --- a/src/description.rs +++ b/src/description.rs @@ -1,11 +1,12 @@ //! Reference entry description. +use crate::variable_list::{Error as VariableListError, VariableList}; use crate::xml::element::{Elements, FromElements, Tagged}; /// Reference entry description. -#[derive(Debug)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct Description { - paragraphs: Vec, + parts: Vec, } impl Description @@ -14,16 +15,14 @@ impl Description #[must_use] pub fn new() -> Self { - Self { - paragraphs: Vec::new(), - } + Self { parts: Vec::new() } } - /// Returns the reference description's paragraphs. + /// Returns the description's parts. #[must_use] - pub fn paragraphs(&self) -> &[Paragraph] + pub fn parts(&self) -> &[Part] { - &self.paragraphs + &self.parts } } @@ -41,15 +40,27 @@ impl FromElements for Description fn from_elements(elements: &Elements) -> Result { - let paragraphs = elements - .get_all_tagged_elements_with_name("para") + let parts = elements + .get_all_tagged_elements() .into_iter() - .map(|paragraph_element| { - Paragraph::from_elements(paragraph_element.child_elements()) + .filter_map(|part_elem| match part_elem.name() { + "para" => Some( + Paragraph::from_elements(part_elem.child_elements()) + .map(Part::Paragraph) + .map_err(Self::Error::InvalidParagraph), + ), + + "variablelist" => Some( + VariableList::from_elements(part_elem.child_elements()) + .map(Part::VariableList) + .map_err(Self::Error::InvalidVariableList), + ), + "title" => None, + name => Some(Err(Self::Error::UnknownPartFound(name.to_string()))), }) - .collect::, _>>()?; + .collect::, Self::Error>>()?; - Ok(Description { paragraphs }) + Ok(Description { parts }) } } @@ -57,13 +68,32 @@ impl FromElements for Description #[derive(Debug, thiserror::Error)] pub enum Error { + /// Unknown part element found. + #[error("Unknown part element with name '{0}' found")] + UnknownPartFound(String), + /// Invalid paragraph. #[error("Invalid paragraph")] - InvalidParagraph(#[from] ParagraphError), + InvalidParagraph(#[source] ParagraphError), + + /// Invalid variable list. + #[error("Invalid variable list")] + InvalidVariableList(#[source] VariableListError), +} + +/// Description part. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Part +{ + /// Paragraph. + Paragraph(Paragraph), + + /// Variable list. + VariableList(VariableList), } /// Reference entry description paragraph. -#[derive(Debug)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct Paragraph { parts: Vec, @@ -71,6 +101,14 @@ pub struct Paragraph impl Paragraph { + /// Returns a new `Paragraph`. + pub fn new(parts: impl IntoIterator) -> Self + { + Self { + parts: parts.into_iter().collect(), + } + } + /// Returns the parts of the paragraph. #[must_use] pub fn parts(&self) -> &[ParagraphPart] @@ -106,7 +144,7 @@ pub enum ParagraphError } /// Reference entry description paragraph part. -#[derive(Debug)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum ParagraphPart { /// Text part. @@ -123,6 +161,9 @@ pub enum ParagraphPart /// Reference entry citation part. Entry(String), + + /// Variable list part. + VariableList(VariableList), } impl FromElements for ParagraphPart @@ -154,6 +195,9 @@ impl ParagraphPart "function" => Self::Function, "parameter" => Self::Parameter, "citerefentry" => Self::Entry, + "variablelist" => |_| { + unreachable!(); + }, _ => { return Err(::Error::UnknownPart( tagged_element.name().to_string(), @@ -175,6 +219,13 @@ impl ParagraphPart return Ok(Self::Entry(title.clone())); } + if tagged_element.name() == "variablelist" { + let variable_list = + VariableList::from_elements(tagged_element.child_elements())?; + + return Ok(Self::VariableList(variable_list)); + } + let text_element = tagged_element .child_elements() .get_first_text_element() @@ -203,4 +254,8 @@ pub enum ParagraphPartError /// No entry title found. #[error("No entry title found")] NoEntryTitleFound, + + /// Invalid variable list. + #[error("Invalid variable list")] + InvalidVariableList(#[from] VariableListError), } diff --git a/src/lib.rs b/src/lib.rs index 11d376c..b6343de 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,6 +14,7 @@ use crate::xml::element::{Attribute, Elements, FromElements}; use crate::xml::parser::{Error as ParserError, Parser}; pub mod description; +pub mod variable_list; mod xml; diff --git a/src/variable_list.rs b/src/variable_list.rs new file mode 100644 index 0000000..2c839f0 --- /dev/null +++ b/src/variable_list.rs @@ -0,0 +1,264 @@ +//! Variable list. + +use crate::description::{Paragraph, ParagraphError, ParagraphPart, ParagraphPartError}; +use crate::xml::element::{Element, FromElements}; + +/// Variable list. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct VariableList +{ + entries: Vec, +} + +impl VariableList +{ + /// Returns a new `VariableList`. + pub fn new(entries: impl IntoIterator) -> Self + { + Self { + entries: entries.into_iter().collect(), + } + } + + /// Returns the variable list entries. + #[must_use] + pub fn entries(&self) -> &[Entry] + { + &self.entries + } +} + +impl FromElements for VariableList +{ + type Error = Error; + + fn from_elements( + elements: &crate::xml::element::Elements, + ) -> Result + { + let entries = elements + .get_all_tagged_elements_with_name("varlistentry") + .into_iter() + .map(|entry_elem| Entry::from_elements(entry_elem.child_elements())) + .collect::, _>>()?; + + Ok(Self { entries }) + } +} + +/// [`VariableList`] error. +#[derive(Debug, thiserror::Error)] +pub enum Error +{ + /// Invalid entry. + #[error("Invalid entry")] + InvalidEntry(#[from] EntryError), +} + +/// Variable list entry. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Entry +{ + term: ParagraphPart, + item: Vec, +} + +impl Entry +{ + /// Returns a new `Entry`. + pub fn new(term: ParagraphPart, item: impl IntoIterator) -> Self + { + Self { + term, + item: item.into_iter().collect(), + } + } + + /// Returns the variable list entry term. + #[must_use] + pub fn term(&self) -> &ParagraphPart + { + &self.term + } + + /// Returns the variable list entry item. + #[must_use] + pub fn item(&self) -> &[Paragraph] + { + &self.item + } +} + +impl FromElements for Entry +{ + type Error = EntryError; + + fn from_elements( + elements: &crate::xml::element::Elements, + ) -> Result + { + let term_element = elements + .get_first_tagged_with_name("term") + .ok_or(Self::Error::MissingTerm)?; + + let term = ParagraphPart::from_elements(term_element.child_elements()) + .map_err(|err| Self::Error::InvalidTerm(Box::new(err)))?; + + let item_element = elements + .get_first_tagged_with_name("listitem") + .ok_or(Self::Error::MissingListItem)?; + + let item = item_element + .child_elements() + .into_iter() + .filter_map(|elem| match elem { + Element::Tagged(tagged_elem) if tagged_elem.name() == "para" => { + Some(tagged_elem) + } + _ => None, + }) + .map(|para_elem| Paragraph::from_elements(para_elem.child_elements())) + .collect::, _>>() + .map_err(|err| Self::Error::InvalidItemParagraph(Box::new(err)))?; + + Ok(Self { term, item }) + } +} + +/// [`Entry`] error. +#[derive(Debug, thiserror::Error)] +pub enum EntryError +{ + /// Missing tagged element with name 'term'. + #[error("Missing tagged element with name 'term'")] + MissingTerm, + + /// Missing tagged element with name 'listitem'. + #[error("Missing tagged element with name 'listitem'")] + MissingListItem, + + /// Invalid term. + #[error("Invalid term")] + InvalidTerm(#[source] Box), + + /// Invalid item paragraph. + #[error("Invalid item paragraph")] + InvalidItemParagraph(#[source] Box), +} + +#[cfg(test)] +mod tests +{ + use super::*; + use crate::xml::element::{Element, Elements, Tagged}; + + #[test] + fn variable_list_from_elements_works() + { + let variable_list = VariableList::from_elements(&Elements::from([ + Element::Tagged(Tagged::new( + &"varlistentry", + [ + Element::Tagged(Tagged::new( + &"term", + [Element::Text("foobar".to_string())], + [], + )), + Element::Tagged(Tagged::new( + &"listitem", + [Element::Tagged(Tagged::new( + &"para", + [Element::Text("Hello hello.".to_string())], + [], + ))], + [], + )), + ], + [], + )), + Element::Tagged(Tagged::new( + &"varlistentry", + [ + Element::Tagged(Tagged::new( + &"term", + [Element::Text("Hello there".to_string())], + [], + )), + Element::Tagged(Tagged::new( + &"listitem", + [Element::Tagged(Tagged::new( + &"para", + [ + Element::Text("Tosche station".to_string()), + Element::Text("Power converters".to_string()), + ], + [], + ))], + [], + )), + ], + [], + )), + Element::Tagged(Tagged::new( + &"varlistentry", + [ + Element::Tagged(Tagged::new( + &"term", + [Element::Text("There is another".to_string())], + [], + )), + Element::Tagged(Tagged::new( + &"listitem", + [Element::Tagged(Tagged::new( + &"para", + [Element::Text("It's a trap".to_string())], + [], + ))], + [], + )), + ], + [], + )), + ])) + .expect("Expected Ok"); + + assert_eq!(variable_list.entries.len(), 3); + } + + #[test] + fn entry_from_elements_works() + { + let entry = Entry::from_elements(&Elements::from([ + Element::Tagged(Tagged::new(&"term", [Element::Text("Foo".to_string())], [])), + Element::Tagged(Tagged::new( + &"listitem", + [ + Element::Tagged(Tagged::new( + &"para", + [Element::Text("bar".to_string())], + [], + )), + Element::Tagged(Tagged::new( + &"para", + [Element::Text("Hello there.".to_string())], + [], + )), + ], + [], + )), + ])) + .expect("Expected Ok"); + + assert!( + matches!(entry.term, ParagraphPart::Text(para_part) if para_part == "Foo") + ); + + assert_eq!( + entry.item, + vec![ + Paragraph::new([ParagraphPart::Text("bar".to_string())]), + Paragraph::new([ParagraphPart::Text("Hello there.".to_string())]), + ] + ); + } +} diff --git a/src/xml/element.rs b/src/xml/element.rs index 647fe90..b778dac 100644 --- a/src/xml/element.rs +++ b/src/xml/element.rs @@ -44,6 +44,17 @@ impl Elements }) } + pub fn get_all_tagged_elements(&self) -> Vec<&Tagged> + { + self.elements + .iter() + .filter_map(|element| match element { + Element::Tagged(tagged_element) => Some(tagged_element), + _ => None, + }) + .collect() + } + pub fn get_all_tagged_elements_with_name(&self, tag_name: &str) -> Vec<&Tagged> { self.elements -- cgit v1.2.3-18-g5258