From 7c5bec7db2a2fc8c796d5f31bdeb03da0946133d Mon Sep 17 00:00:00 2001 From: HampusM Date: Sun, 19 Feb 2023 13:49:41 +0100 Subject: feat: add project & registry parsing /w commands --- src/command.rs | 263 +++++++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 158 ++++++++++++++++++++++++++++++++ src/xml/element.rs | 144 +++++++++++++++++++++++++++++ src/xml/mod.rs | 2 + src/xml/parser.rs | 195 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 762 insertions(+) create mode 100644 src/command.rs create mode 100644 src/lib.rs create mode 100644 src/xml/element.rs create mode 100644 src/xml/mod.rs create mode 100644 src/xml/parser.rs (limited to 'src') diff --git a/src/command.rs b/src/command.rs new file mode 100644 index 0000000..c7ada95 --- /dev/null +++ b/src/command.rs @@ -0,0 +1,263 @@ +//! OpenGL command. +use crate::xml::element::{Elements, FromElements}; + +/// A command. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Command +{ + prototype: Prototype, + parameters: Vec, +} + +impl Command +{ + /// Returns a new `Command`. + pub fn new( + prototype: Prototype, + parameters: impl IntoIterator, + ) -> Self + { + Self { + prototype, + parameters: parameters.into_iter().collect(), + } + } + + /// Returns the command prototype. + #[must_use] + pub fn prototype(&self) -> &Prototype + { + &self.prototype + } + + /// Returns the command parameters. + #[must_use] + pub fn parameters(&self) -> &[Parameter] + { + &self.parameters + } +} + +impl FromElements for Command +{ + type Error = Error; + + fn from_elements( + elements: &crate::xml::element::Elements, + ) -> Result + { + let proto_element = elements + .get_first_tagged_element("proto") + .ok_or(Self::Error::MissingPrototype)?; + + let prototype = Prototype::from_elements(proto_element.child_elements())?; + + let parameters = elements + .get_all_tagged_elements_with_name("param") + .into_iter() + .map(|param_element| Parameter::from_elements(param_element.child_elements())) + .collect::, _>>()?; + + Ok(Self { + prototype, + parameters, + }) + } +} + +/// [`Command`] error. +#[derive(Debug, thiserror::Error)] +pub enum Error +{ + /// No 'proto' element was found. + #[error("No 'proto' element was found")] + MissingPrototype, + + /// Invalid prototype. + #[error("Invalid prototype")] + InvalidPrototype(#[from] PrototypeError), + + /// Invalid parameter. + #[error("Invalid parameter")] + InvalidParameter(#[from] ParameterError), +} + +/// A command prototype. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Prototype +{ + name: String, + return_type: String, +} + +impl Prototype +{ + /// Returns a new `Prototype`. + pub fn new(name: impl Into, return_type: impl Into) -> Self + { + Self { + name: name.into(), + return_type: return_type.into(), + } + } + + /// Returns the command prototype name. + #[must_use] + pub fn name(&self) -> &str + { + &self.name + } + + /// Returns the command prototype return type. + #[must_use] + pub fn return_type(&self) -> &str + { + &self.return_type + } +} + +impl FromElements for Prototype +{ + type Error = PrototypeError; + + fn from_elements( + elements: &crate::xml::element::Elements, + ) -> Result + { + let name = elements + .get_first_tagged_element("name") + .ok_or(Self::Error::MissingName)? + .child_elements() + .get_first_text_element() + .cloned() + .unwrap_or_default(); + + let return_type = find_type(elements); + + Ok(Self { name, return_type }) + } +} + +/// [`Prototype`] error. +#[derive(Debug, thiserror::Error)] +pub enum PrototypeError +{ + /// No 'name' element was found. + #[error("No 'name' element was found")] + MissingName, +} + +/// A command parameter. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Parameter +{ + name: String, + ty: String, +} + +impl Parameter +{ + /// Returns a new `Parameter`. + pub fn new(name: impl Into, ty: impl Into) -> Self + { + Self { + name: name.into(), + ty: ty.into(), + } + } + + /// Returns the name of the command parameter. + #[must_use] + pub fn name(&self) -> &str + { + &self.name + } + + /// Returns the type of the command parameter. + #[must_use] + pub fn get_type(&self) -> &str + { + &self.ty + } +} + +impl FromElements for Parameter +{ + type Error = ParameterError; + + fn from_elements(elements: &Elements) -> Result + { + let name = elements + .get_first_tagged_element("name") + .ok_or(Self::Error::MissingName)? + .child_elements() + .get_first_text_element() + .cloned() + .unwrap_or_default(); + + let ty = find_type(elements); + + Ok(Self { name, ty }) + } +} + +/// [`Parameter`] error. +#[derive(Debug, thiserror::Error)] +pub enum ParameterError +{ + /// No 'name' element was found. + #[error("No 'name' element was found")] + MissingName, +} + +fn find_type(elements: &Elements) -> String +{ + let text_type_parts = elements + .get_all_text_elements() + .into_iter() + .map(|text_type_part| text_type_part.trim()) + .filter(|text_type_part| !text_type_part.is_empty()) + .collect::>(); + + let opt_ptype_text = get_ptype_text(elements); + + opt_ptype_text.map_or_else( + || join_space_strs(text_type_parts.iter()), + |ptype_text| { + let Some(first_part) = text_type_parts.first() else { + return ptype_text.clone(); + }; + + let before = if *first_part == "const" { "const " } else { "" }; + + let after_start_index = usize::from(*first_part == "const"); + + format!( + "{before}{ptype_text} {}", + text_type_parts + .get(after_start_index..) + .map(|parts| join_space_strs(parts.iter())) + .unwrap_or_default() + ) + }, + ) +} + +fn get_ptype_text(elements: &Elements) -> Option<&String> +{ + let ptype_element = elements.get_first_tagged_element("ptype")?; + + ptype_element.child_elements().get_first_text_element() +} + +fn join_space_strs(strings: Strings) -> String +where + Strings: Iterator, + StrItem: ToString, +{ + strings + .into_iter() + .map(|string| string.to_string()) + .collect::>() + .join(" ") +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..45f9fc9 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,158 @@ +//! Rust API for the [OpenGL API and Extension Registry]. +//! +//! [OpenGL API and Extension Registry]: https://github.com/KhronosGroup/OpenGL-Registry +#![cfg_attr(doc_cfg, feature(doc_cfg))] +#![deny(clippy::all, clippy::pedantic, missing_docs)] + +use std::fs::File; +use std::io::Read; + +use crate::command::{Command, Error as CommandError}; +use crate::xml::element::{Element, Elements, FromElements}; +use crate::xml::parser::{Error as ParserError, Parser}; + +pub mod command; + +mod xml; + +#[cfg(feature = "include-xml")] +const GL_REGISTRY_XML: &str = include_str!("../OpenGL-Registry/xml/gl.xml"); + +const REGISTRY_TAG_NAME: &str = "registry"; + +/// Representation of the OpenGL registry. +pub struct Registry +{ + commands: Vec, +} + +impl Registry +{ + /// Retrieves the OpenGL registry from a included XML file. + /// + /// # Errors + /// Returns `Err` if parsing fails in any way. + #[cfg(feature = "include-xml")] + #[cfg_attr(doc_cfg, doc(cfg(feature = "include-xml")))] + pub fn retrieve() -> Result + { + Self::retrieve_from_bytes(GL_REGISTRY_XML.as_bytes()) + } + + /// Retrieves the OpenGL registry from XML bytes. + /// + /// # Errors + /// Returns `Err` if parsing fails in any way. + pub fn retrieve_from_bytes(xml_bytes: &[u8]) -> Result + { + let mut parser = Parser::new(xml_bytes); + + let elements = parser.parse().map_err(ParsingError)?; + + let registry_element = elements + .get_first_tagged_element(REGISTRY_TAG_NAME) + .ok_or(RegistryError::MissingRegistryElement)?; + + let registry = Registry::from_elements(registry_element.child_elements())?; + + Ok(registry) + } + + /// Retrieves the OpenGL registry from a XML file. + /// + /// # Errors + /// Returns `Err` if: + /// - Parsing fails in any way. + /// - An I/O error occurs. + pub fn retrieve_from_file(xml_file: &mut File) -> Result + { + let mut buf = Vec::new(); + + xml_file.read_to_end(&mut buf)?; + + Self::retrieve_from_bytes(&buf) + } + + /// Creates a new `Registry`. + /// + /// # Note + /// This will **NOT** use anything from the actual OpenGL registry. Use the + /// [`Registry::retrieve`] method for that. + pub fn new(commands: impl IntoIterator) -> Self + { + Self { + commands: commands.into_iter().collect(), + } + } + + /// Returns the available commands. + #[must_use] + pub fn commands(&self) -> &[Command] + { + &self.commands + } +} + +impl FromElements for Registry +{ + type Error = RegistryError; + + fn from_elements(elements: &Elements) -> Result + { + let commands_element = elements + .get_first_tagged_element("commands") + .ok_or(Self::Error::MissingCommandsElement)?; + + let command_elements = + commands_element + .child_elements() + .into_iter() + .filter_map(|element| match element { + Element::Tagged(tagged_element) + if tagged_element.name() == "command" => + { + Some(tagged_element) + } + _ => None, + }); + + let commands = command_elements + .into_iter() + .map(|command_element| { + Command::from_elements(command_element.child_elements()) + }) + .collect::, _>>()?; + + Ok(Self { commands }) + } +} + +/// [`Registry`] error. +#[derive(Debug, thiserror::Error)] +pub enum RegistryError +{ + /// No 'registry' element was found. + #[error("No 'registry' element was found")] + MissingRegistryElement, + + /// No 'commands' element was found. + #[error("No 'commands' element was found")] + MissingCommandsElement, + + /// A command is invalid. + #[error("Invalid command")] + InvalidCommand(#[from] CommandError), + + /// Parsing failed. + #[error("Parsing failed")] + ParsingFailed(#[from] ParsingError), + + /// I/O failed. + #[error("I/O failed")] + IOFailed(#[from] std::io::Error), +} + +/// Parsing error. +#[derive(Debug, thiserror::Error)] +#[error(transparent)] +pub struct ParsingError(#[from] ParserError); diff --git a/src/xml/element.rs b/src/xml/element.rs new file mode 100644 index 0000000..f469480 --- /dev/null +++ b/src/xml/element.rs @@ -0,0 +1,144 @@ +#[derive(Debug, PartialEq, Eq)] +pub struct Elements +{ + elements: Vec, +} + +impl Elements +{ + pub fn get_first_tagged_element(&self, tag_name: &str) -> Option<&Tagged> + { + self.elements.iter().find_map(|element| match element { + Element::Tagged(tagged_element) if tagged_element.name == tag_name => { + Some(tagged_element) + } + _ => None, + }) + } + + pub fn get_all_tagged_elements_with_name(&self, tag_name: &str) -> Vec<&Tagged> + { + self.elements + .iter() + .filter_map(|element| match element { + Element::Tagged(tagged_element) if tagged_element.name == tag_name => { + Some(tagged_element) + } + _ => None, + }) + .collect() + } + + pub fn get_first_text_element(&self) -> Option<&String> + { + self.elements.iter().find_map(|element| match element { + Element::Text(text) => Some(text), + _ => None, + }) + } + + pub fn get_all_text_elements(&self) -> Vec<&String> + { + self.elements + .iter() + .filter_map(|element| match element { + Element::Text(text) => Some(text), + _ => None, + }) + .collect() + } + + pub fn has_tagged_element(&self, tag_name: &str) -> bool + { + self.elements.iter().any(|element| { + matches!( + element, + Element::Tagged(tagged_element) if tagged_element.name == tag_name + ) + }) + } +} + +impl> From for Elements +{ + fn from(into_iter: IntoIter) -> Self + { + Self { + elements: into_iter.into_iter().collect(), + } + } +} + +impl<'elements> IntoIterator for &'elements Elements +{ + type IntoIter = Iter<'elements>; + type Item = &'elements Element; + + fn into_iter(self) -> Self::IntoIter + { + Self::IntoIter { + elements: self.elements.iter(), + } + } +} + +pub struct Iter<'elements> +{ + elements: std::slice::Iter<'elements, Element>, +} + +impl<'elements> Iterator for Iter<'elements> +{ + type Item = &'elements Element; + + fn next(&mut self) -> Option + { + self.elements.next() + } +} + +#[derive(Debug, PartialEq, Eq)] +pub enum Element +{ + Tagged(Tagged), + Text(String), + Comment(String), +} + +#[derive(Debug, PartialEq, Eq)] +pub struct Tagged +{ + name: String, + child_elements: Elements, +} + +impl Tagged +{ + pub fn new(name: &Name, child_elements: ChildElements) -> Self + where + Name: ToString, + ChildElements: Into, + { + Self { + name: name.to_string(), + child_elements: child_elements.into(), + } + } + + pub fn name(&self) -> &str + { + &self.name + } + + pub fn child_elements(&self) -> &Elements + { + &self.child_elements + } +} + +pub trait FromElements: Sized +{ + type Error; + + fn from_elements(elements: &Elements) -> Result; +} diff --git a/src/xml/mod.rs b/src/xml/mod.rs new file mode 100644 index 0000000..12368c3 --- /dev/null +++ b/src/xml/mod.rs @@ -0,0 +1,2 @@ +pub mod element; +pub mod parser; diff --git a/src/xml/parser.rs b/src/xml/parser.rs new file mode 100644 index 0000000..d152a6e --- /dev/null +++ b/src/xml/parser.rs @@ -0,0 +1,195 @@ +use std::io::BufRead; + +use quick_xml::events::{BytesStart, BytesText, Event}; +use quick_xml::Reader; + +use crate::xml::element::{Element, Elements, Tagged}; + +/// XML parser. +pub struct Parser +{ + reader: Reader, +} + +impl Parser +{ + pub fn new(src: Source) -> Self + { + Self { + reader: Reader::from_reader(src), + } + } + + pub fn parse(&mut self) -> Result + { + let mut buf = Vec::new(); + + let mut elements = Vec::new(); + + loop { + let event = self.reader.read_event_into(&mut buf)?; + + let element = match self.handle_event(event)? { + EventHandlingResult::Element(element) => element, + EventHandlingResult::Event(_) => { + continue; + } + EventHandlingResult::End => { + break; + } + }; + + elements.push(element); + } + + Ok(elements.into()) + } + + fn parse_text(text: &BytesText) -> Result + { + String::from_utf8(text.to_vec()).map_err(|_| Error::TextNotUTF8) + } + + fn parse_tagged(&mut self, start: &BytesStart) -> Result + { + let mut child_elements = Vec::new(); + + let mut buf = Vec::new(); + + loop { + let event = self.reader.read_event_into(&mut buf)?; + + match event { + Event::End(end) if end.name() == start.name() => { + break; + } + event => match self.handle_event(event)? { + EventHandlingResult::Element(element) => { + child_elements.push(element); + } + EventHandlingResult::End => { + return Err(Error::UnexpectedEndOfFile); + } + EventHandlingResult::Event(_) => {} + }, + } + } + + Ok(Element::Tagged(Tagged::new( + &String::from_utf8(start.name().as_ref().to_vec()) + .map_err(|_| Error::TagNameNotUTF8)?, + child_elements, + ))) + } + + fn handle_event<'a>( + &'a mut self, + event: Event<'a>, + ) -> Result + { + match event { + Event::Text(text) => Ok(EventHandlingResult::Element(Element::Text( + Self::parse_text(&text)?, + ))), + Event::Start(start) => { + Ok(EventHandlingResult::Element(self.parse_tagged(&start)?)) + } + Event::End(_) => Err(Error::UnexpectedTagEnd), + Event::Eof => Ok(EventHandlingResult::End), + Event::Comment(comment_text) => Ok(EventHandlingResult::Element( + Element::Comment(Self::parse_text(&comment_text)?), + )), + event => Ok(EventHandlingResult::Event(event)), + } + } +} + +#[derive(Debug, thiserror::Error)] +pub enum Error +{ + #[error(transparent)] + QuickXMLFailed(#[from] quick_xml::Error), + + #[error("Text is not UTF-8")] + TextNotUTF8, + + #[error("Tag name is not UTF-8")] + TagNameNotUTF8, + + #[error("Unexpectedly found the end of a tag")] + UnexpectedTagEnd, + + #[error("Unexpected end of file")] + UnexpectedEndOfFile, +} + +enum EventHandlingResult<'event> +{ + Element(Element), + Event(Event<'event>), + End, +} + +#[cfg(test)] +mod tests +{ + use pretty_assertions::assert_eq; + + use super::*; + + #[test] + fn can_parse() + { + let mut parser = Parser::new("Hello there".as_bytes()); + + assert_eq!( + parser.parse().expect("Expected Ok"), + Elements::from(vec![Element::Tagged(Tagged::new( + &"foo", + vec![Element::Text("Hello there".to_string())] + ))]) + ); + + let mut parser = Parser::new("123 Hello".as_bytes()); + + assert_eq!( + parser.parse().expect("Expected Ok"), + Elements::from(vec![Element::Tagged(Tagged::new( + &"foo", + vec![ + Element::Tagged(Tagged::new( + &"bar", + Elements::from(vec![Element::Text("123".to_string())]) + )), + Element::Text(" Hello".to_string()) + ] + ))]) + ); + + let mut parser = Parser::new("".as_bytes()); + + assert_eq!( + parser.parse().expect("Expected Ok"), + Elements::from(Vec::new()) + ); + + let mut parser = Parser::new( + "Hello there123".as_bytes(), + ); + + assert_eq!( + parser.parse().expect("Expected Ok"), + Elements::from(vec![Element::Tagged(Tagged::new( + &"foo", + vec![ + Element::Comment("XML is awful".to_string()), + Element::Text("Hello there".to_string()), + Element::Tagged(Tagged::new( + &"bar", + vec![Element::Text("123".to_string())] + )), + ] + ))]) + ); + } +} -- cgit v1.2.3-18-g5258