From 86f41e49f87764f9afd4be1d0d008a320dcfc331 Mon Sep 17 00:00:00 2001 From: HampusM Date: Sun, 26 Mar 2023 18:50:31 +0200 Subject: feat: add deserializing API interface definitions --- src/api_interface_definition.rs | 773 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 773 insertions(+) create mode 100644 src/api_interface_definition.rs (limited to 'src/api_interface_definition.rs') diff --git a/src/api_interface_definition.rs b/src/api_interface_definition.rs new file mode 100644 index 0000000..441d3f7 --- /dev/null +++ b/src/api_interface_definition.rs @@ -0,0 +1,773 @@ +//! GL API interface definition. + +use quick_xml::events::BytesStart; + +use crate::deserialization::{Deserialize, Deserializer, DeserializerError}; +use crate::DeserializationError; + +/// GL API interface definition. +#[derive(Debug, PartialEq, Eq)] +pub struct APIInterfaceDefinition +{ + api_name: String, + name: String, + version: String, + requirements: Vec, + removals: Vec, +} + +impl APIInterfaceDefinition +{ + /// Returns a new `APIInterfaceDefinition`. + #[must_use] + pub fn new( + api_name: impl Into, + name: impl Into, + version: impl Into, + requirements: impl IntoIterator, + removals: impl IntoIterator, + ) -> Self + { + Self { + api_name: api_name.into(), + name: name.into(), + version: version.into(), + requirements: requirements.into_iter().collect(), + removals: removals.into_iter().collect(), + } + } + + /// Returns the API name. + #[must_use] + pub fn api_name(&self) -> &str + { + &self.api_name + } + + /// Returns the name. + #[must_use] + pub fn name(&self) -> &str + { + &self.name + } + + /// Returns the version. + #[must_use] + pub fn version(&self) -> &str + { + &self.version + } + + /// Returns the requirements. + #[must_use] + pub fn requirements(&self) -> &[Requirement] + { + &self.requirements + } + + /// Returns the removals. + #[must_use] + pub fn removals(&self) -> &[Removal] + { + &self.removals + } +} + +impl Deserialize for APIInterfaceDefinition +{ + type Error = Error; + + fn deserialize( + start: &BytesStart, + deserializer: &mut TDeserializer, + ) -> Result + { + let api_name = String::from_utf8( + start + .try_get_attribute(b"api") + .map_err(DeserializerError::ReadFailed)? + .ok_or_else(|| Error::MissingAPIName)? + .value + .into_owned(), + ) + .map_err(|_| Error::APINameNotUTF8)?; + + let name = String::from_utf8( + start + .try_get_attribute(b"name") + .map_err(DeserializerError::ReadFailed)? + .ok_or_else(|| Error::MissingName)? + .value + .into_owned(), + ) + .map_err(|_| Error::NameNotUTF8)?; + + let version = String::from_utf8( + start + .try_get_attribute(b"number") + .map_err(DeserializerError::ReadFailed)? + .ok_or_else(|| Error::MissingVersionNumber)? + .value + .into_owned(), + ) + .map_err(|_| Error::VersionNotUTF8)?; + + let requirements = deserializer.de_tag_list::("require")?; + + let removals = deserializer.de_tag_list::("remove")?; + + Ok(Self { + api_name, + name, + version, + requirements, + removals, + }) + } +} + +/// [`APIInterfaceDefinition`] error. +#[derive(Debug, thiserror::Error)] +pub enum Error +{ + /// Missing API name attribute. + #[error("Missing API name attribute")] + MissingAPIName, + + /// Missing name attribute. + #[error("Missing name attribute")] + MissingName, + + /// Missing version number attribute. + #[error("Missing version number attribute")] + MissingVersionNumber, + + /// API name is not valid UTF-8. + #[error("API name is not valid UTF-8")] + APINameNotUTF8, + + /// Name is not valid UTF-8. + #[error("Name is not valid UTF-8")] + NameNotUTF8, + + /// Version is not valid UTF-8. + #[error("Version is not valid UTF-8")] + VersionNotUTF8, + + /// Deserialization failed. + #[error("Deserialization failed")] + DeserializationFailed(#[from] DeserializationError), +} + +impl From for Error +{ + fn from(err: DeserializerError) -> Self + { + DeserializationError(err).into() + } +} + +/// GL API feature requirement. +#[derive(Debug, PartialEq, Eq)] +pub struct Requirement +{ + profile: Option, + comment: Option, + features: Vec, +} + +impl Requirement +{ + /// Returns a new `Requirement`. + #[must_use] + pub fn new( + profile: Option, + comment: Option, + features: impl IntoIterator, + ) -> Self + { + Self { + profile, + comment, + features: features.into_iter().collect(), + } + } + + /// Returns the required features. + #[must_use] + pub fn features(&self) -> &[Feature] + { + &self.features + } + + /// Returns the profile. + #[must_use] + pub fn profile(&self) -> Option<&str> + { + self.profile.as_deref() + } + + /// Returns the comment. + #[must_use] + pub fn comment(&self) -> Option<&str> + { + self.comment.as_deref() + } +} + +impl Deserialize for Requirement +{ + type Error = RequirementError; + + fn deserialize( + start: &BytesStart, + deserializer: &mut TDeserializer, + ) -> Result + { + let profile = match start + .try_get_attribute(b"profile") + .map_err(DeserializerError::ReadFailed)? + { + Some(comment_attr) => Some( + String::from_utf8(comment_attr.value.into_owned()) + .map_err(|_| RequirementError::ProfileNotUTF8)?, + ), + None => None, + }; + + let comment = match start + .try_get_attribute(b"comment") + .map_err(DeserializerError::ReadFailed)? + { + Some(comment_attr) => Some( + String::from_utf8(comment_attr.value.into_owned()) + .map_err(|_| RequirementError::CommentNotUTF8)?, + ), + None => None, + }; + + let features = deserializer.de_list::()?; + + Ok(Self { + profile, + comment, + features, + }) + } +} + +/// [`Requirement`] error. +#[derive(Debug, thiserror::Error)] +pub enum RequirementError +{ + /// Profile is not valid UTF-8. + #[error("Profile is not valid UTF-8")] + ProfileNotUTF8, + + /// Comment is not valid UTF-8. + #[error("Comment is not valid UTF-8")] + CommentNotUTF8, + + /// Deserialization failed. + #[error("Deserialization failed")] + DeserializationFailed(#[from] DeserializationError), +} + +impl From for RequirementError +{ + fn from(err: DeserializerError) -> Self + { + DeserializationError(err).into() + } +} + +/// GL API feature removal. +#[derive(Debug, PartialEq, Eq)] +pub struct Removal +{ + profile: String, + comment: String, + features: Vec, +} + +impl Removal +{ + /// Returns a new `Removal`. + #[must_use] + pub fn new( + profile: impl Into, + comment: impl Into, + features: impl IntoIterator, + ) -> Self + { + Self { + profile: profile.into(), + comment: comment.into(), + features: features.into_iter().collect(), + } + } + + /// Returns the removed features. + #[must_use] + pub fn features(&self) -> &[Feature] + { + &self.features + } + + /// Returns the profile. + #[must_use] + pub fn profile(&self) -> &str + { + &self.profile + } + + /// Returns the comment. + #[must_use] + pub fn comment(&self) -> &str + { + &self.comment + } +} + +impl Deserialize for Removal +{ + type Error = RemovalError; + + fn deserialize( + start: &BytesStart, + deserializer: &mut TDeserializer, + ) -> Result + { + let profile = String::from_utf8( + start + .try_get_attribute(b"profile") + .map_err(DeserializerError::ReadFailed)? + .ok_or_else(|| RemovalError::MissingProfile)? + .value + .into_owned(), + ) + .map_err(|_| RemovalError::ProfileNotUTF8)?; + + let comment = String::from_utf8( + start + .try_get_attribute(b"comment") + .map_err(DeserializerError::ReadFailed)? + .ok_or_else(|| RemovalError::MissingComment)? + .value + .into_owned(), + ) + .map_err(|_| RemovalError::CommentNotUTF8)?; + + let features = deserializer.de_list::()?; + + Ok(Self { + profile, + comment, + features, + }) + } +} + +/// [`Removal`] error. +#[derive(Debug, thiserror::Error)] +pub enum RemovalError +{ + /// Missing profile attribute. + #[error("Missing profile attribute")] + MissingProfile, + + /// Missing comment attribute. + #[error("Missing comment attribute")] + MissingComment, + + /// Profile is not valid UTF-8. + #[error("Profile is not valid UTF-8")] + ProfileNotUTF8, + + /// Comment is not valid UTF-8. + #[error("Comment is not valid UTF-8")] + CommentNotUTF8, + + /// Deserialization failed. + #[error("Deserialization failed")] + DeserializationFailed(#[from] DeserializationError), +} + +impl From for RemovalError +{ + fn from(err: DeserializerError) -> Self + { + DeserializationError(err).into() + } +} + +/// GL API feature. +#[derive(Debug, PartialEq, Eq)] +pub struct Feature +{ + kind: FeatureKind, + name: String, + comment: Option, +} + +impl Feature +{ + /// Returns a new `Feature`. + #[must_use] + pub fn new( + kind: FeatureKind, + name: impl Into, + comment: Option, + ) -> Self + { + Self { + kind, + name: name.into(), + comment, + } + } + + /// Returns the feature kind. + #[must_use] + pub fn kind(&self) -> FeatureKind + { + self.kind + } + + /// Returns the feature name. + #[must_use] + pub fn name(&self) -> &str + { + &self.name + } + + /// Returns the feature comment. + #[must_use] + pub fn comment(&self) -> Option<&str> + { + self.comment.as_deref() + } +} + +impl Deserialize for Feature +{ + type Error = FeatureError; + + fn deserialize( + start: &BytesStart, + _deserializer: &mut TDeserializer, + ) -> Result + { + let kind = match start.name().as_ref() { + b"enum" => Ok(FeatureKind::Enum), + b"command" => Ok(FeatureKind::Command), + b"type" => Ok(FeatureKind::Type), + unknown_kind => Err(FeatureError::UnknownFeatureKind( + String::from_utf8_lossy(unknown_kind).into_owned(), + )), + }?; + + let name = String::from_utf8( + start + .try_get_attribute(b"name") + .map_err(DeserializerError::ReadFailed)? + .ok_or_else(|| FeatureError::MissingName)? + .value + .into_owned(), + ) + .map_err(|_| FeatureError::NameNotUTF8)?; + + let comment = match start + .try_get_attribute(b"comment") + .map_err(DeserializerError::ReadFailed)? + { + Some(comment_attr) => Some( + String::from_utf8(comment_attr.value.into_owned()) + .map_err(|_| FeatureError::CommentNotUTF8)?, + ), + None => None, + }; + + Ok(Self { + kind, + name, + comment, + }) + } +} + +/// [`Feature`] error. +#[derive(Debug, thiserror::Error)] +pub enum FeatureError +{ + /// Unknown feature kind. + #[error("Unknown feature kind {0}")] + UnknownFeatureKind(String), + + /// Missing name attribute. + #[error("Missing name attribute")] + MissingName, + + /// Name is not valid UTF-8. + #[error("Name is not valid UTF-8")] + NameNotUTF8, + + /// Comment is not valid UTF-8. + #[error("Comment is not valid UTF-8")] + CommentNotUTF8, + + /// Deserialization failed. + #[error("Deserialization failed")] + DeserializationFailed(#[from] DeserializationError), +} + +impl From for FeatureError +{ + fn from(err: DeserializerError) -> Self + { + DeserializationError(err).into() + } +} + +/// GL API feature kind. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FeatureKind +{ + /// A enum. + Enum, + + /// A command. + Command, + + /// A type. + Type, +} + +#[cfg(test)] +mod tests +{ + use pretty_assertions::{assert_eq, assert_str_eq}; + + use super::*; + use crate::deserialization::MockDeserializer; + + #[test] + fn deserialize_feature_works() + { + let mut mock_deserializer = MockDeserializer::new(); + + let commentless_enum_feature = Feature::deserialize( + &BytesStart::new("enum").with_attributes([("name", "GL_BLEND")]), + &mut mock_deserializer, + ) + .expect("Expected Ok"); + + assert_eq!(commentless_enum_feature.kind, FeatureKind::Enum); + assert_str_eq!(commentless_enum_feature.name, "GL_BLEND"); + assert!(commentless_enum_feature.comment.is_none()); + + let enum_feature = Feature::deserialize( + &BytesStart::new("enum").with_attributes([ + ("name", "GL_VERTEX_BINDING_BUFFER"), + ("comment", "Added in 2013/10/22 update to the spec"), + ]), + &mut mock_deserializer, + ) + .expect("Expected Ok"); + + assert_eq!(enum_feature.kind, FeatureKind::Enum); + assert_str_eq!(enum_feature.name, "GL_VERTEX_BINDING_BUFFER"); + + assert_str_eq!( + enum_feature.comment.expect("Expected Some"), + "Added in 2013/10/22 update to the spec" + ); + + let command_feature = Feature::deserialize( + &BytesStart::new("command").with_attributes([("name", "glCreateBuffers")]), + &mut mock_deserializer, + ) + .expect("Expected Ok"); + + assert_eq!(command_feature.kind, FeatureKind::Command); + assert_str_eq!(command_feature.name, "glCreateBuffers"); + assert!(command_feature.comment.is_none()); + + let type_feature = Feature::deserialize( + &BytesStart::new("type").with_attributes([("name", "GLbyte")]), + &mut mock_deserializer, + ) + .expect("Expected Ok"); + + assert_eq!(type_feature.kind, FeatureKind::Type); + assert_str_eq!(type_feature.name, "GLbyte"); + assert!(type_feature.comment.is_none()); + + assert!(matches!( + Feature::deserialize(&BytesStart::new("foo"), &mut mock_deserializer), + Err(FeatureError::UnknownFeatureKind(_)) + )); + } + + #[test] + fn deserialize_removal_works() + { + let mut mock_deserializer = MockDeserializer::new(); + + mock_deserializer.expect_de_list().times(1).returning(|_| { + Ok(vec![ + Feature::new(FeatureKind::Command, "glNewList", None), + Feature::new(FeatureKind::Command, "glEndList", None), + ]) + }); + + let removal = Removal::deserialize( + &BytesStart::new("remove").with_attributes([ + ("profile", "core"), + ( + "comment", + "Compatibility-only GL 1.0 features removed from GL 3.2", + ), + ]), + &mut mock_deserializer, + ) + .expect("Expected Ok"); + + assert_str_eq!(removal.profile, "core"); + + assert_str_eq!( + removal.comment, + "Compatibility-only GL 1.0 features removed from GL 3.2" + ); + + assert_eq!(removal.features.len(), 2); + } + + #[test] + fn deserialize_requirement_works() + { + let mut mock_deserializer = MockDeserializer::new(); + + mock_deserializer.expect_de_list().times(1).returning(|_| { + Ok(vec![ + Feature::new(FeatureKind::Command, "glNewList", None), + Feature::new(FeatureKind::Command, "glEndList", None), + ]) + }); + + mock_deserializer.expect_de_list().times(1).returning(|_| { + Ok(vec![ + Feature::new(FeatureKind::Enum, "GL_COLOR_TABLE", None), + Feature::new(FeatureKind::Enum, "GL_MINMAX", None), + Feature::new(FeatureKind::Enum, "GL_HISTOGRAM", None), + ]) + }); + + mock_deserializer.expect_de_list().times(1).returning(|_| { + Ok(vec![ + Feature::new(FeatureKind::Enum, "GL_STACK_UNDERFLOW", None), + Feature::new(FeatureKind::Enum, "GL_STACK_OVERFLOW", None), + ]) + }); + + let requirement = + Requirement::deserialize(&BytesStart::new("require"), &mut mock_deserializer) + .expect("Expected Ok"); + + assert!(requirement.profile.is_none()); + assert!(requirement.comment.is_none()); + + assert_eq!(requirement.features.len(), 2); + + let requirement = Requirement::deserialize( + &BytesStart::new("require").with_attributes([("profile", "compatibility")]), + &mut mock_deserializer, + ) + .expect("Expected Ok"); + + assert_str_eq!(requirement.profile.expect("Expected Some"), "compatibility"); + assert!(requirement.comment.is_none()); + + assert_eq!(requirement.features.len(), 3); + + let requirement = Requirement::deserialize( + &BytesStart::new("require").with_attributes([ + ("profile", "core"), + ( + "comment", + concat!( + "Restore functionality removed in GL 3.2 core ", + "to GL 4.3. Needed for debug interface." + ), + ), + ]), + &mut mock_deserializer, + ) + .expect("Expected Ok"); + + assert_str_eq!(requirement.profile.expect("Expected Some"), "core"); + assert_str_eq!( + requirement.comment.expect("Expected Some"), + concat!( + "Restore functionality removed in GL 3.2 core to ", + "GL 4.3. Needed for debug interface." + ) + ); + + assert_eq!(requirement.features.len(), 2); + } + + #[test] + fn deserialize_api_interface_definition_works() + { + let mut mock_deserializer = MockDeserializer::new(); + + mock_deserializer + .expect_de_tag_list() + .times(1) + .returning(|_, _| { + Ok(vec![ + Requirement::new( + None, + None, + vec![ + Feature::new(FeatureKind::Command, "glFenceSync", None), + Feature::new(FeatureKind::Enum, "GL_WAIT_FAILED", None), + ], + ), + Requirement::new( + None, + Some("Reuse ARB_sync".to_string()), + [Feature::new(FeatureKind::Enum, "GL_OBJECT_TYPE", None)], + ), + ]) + }); + + mock_deserializer + .expect_de_tag_list() + .times(1) + .returning(|_, _| { + Ok(vec![Removal::new( + "core", + "Compatibility-only GL 1.0 features removed from GL 3.2", + vec![Feature::new(FeatureKind::Command, "glBegin", None)], + )]) + }); + + let api_interface_definition = APIInterfaceDefinition::deserialize( + &BytesStart::new("feature").with_attributes([ + ("api", "gl"), + ("name", "GL_VERSION_3_2"), + ("number", "3.2"), + ]), + &mut mock_deserializer, + ) + .expect("Expected Ok"); + + assert_str_eq!(api_interface_definition.api_name, "gl"); + assert_str_eq!(api_interface_definition.name, "GL_VERSION_3_2"); + assert_str_eq!(api_interface_definition.version, "3.2"); + assert_eq!(api_interface_definition.requirements.len(), 2); + assert_eq!(api_interface_definition.removals.len(), 1); + } +} -- cgit v1.2.3-18-g5258