//! GL API interface definition. use std::borrow::Cow; use xml_stinks::deserializer::Deserializer; use xml_stinks::tagged::{TagStart, TagStartError}; use xml_stinks::{impl_from_deserializer_error, DeserializeTagged}; use crate::util::impl_from_deserializer_err_wrapped; 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 DeserializeTagged for APIInterfaceDefinition { type Error = Error; fn deserialize( start: &TagStart, deserializer: &mut TDeserializer, ) -> Result { let api_name = start .get_attribute("api")? .ok_or_else(|| Error::MissingAPIName)? .value() .map_err(|_| Error::APINameNotUTF8)? .into_owned(); let name = start .get_attribute("name")? .ok_or_else(|| Error::MissingName)? .value() .map_err(|_| Error::NameNotUTF8)? .into_owned(); let version = start .get_attribute("number")? .ok_or_else(|| Error::MissingVersionNumber)? .value() .map_err(|_| Error::VersionNotUTF8)? .into_owned(); let requirements = deserializer.de_tag_list::(Some("require"))?; let removals = deserializer.de_tag_list::(Some("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, /// Invalid requirement. #[error("Invalid requirement")] InvalidRequirement(#[from] RequirementError), /// Invalid removal. #[error("Invalid removal")] InvalidRemoval(#[from] RemovalError), /// Deserialization failed. #[error("Deserialization failed")] DeserializationFailed(#[from] DeserializationError), /// Invalid tag start. #[error("Invalid tag start")] InvalidTagStart(#[from] TagStartError), } impl_from_deserializer_err_wrapped!(Error); impl_from_deserializer_error!(Error); /// 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 DeserializeTagged for Requirement { type Error = RequirementError; fn deserialize( start: &TagStart, deserializer: &mut TDeserializer, ) -> Result { let profile = match start.get_attribute("profile")? { Some(comment_attr) => Some( comment_attr .value() .map_err(|_| RequirementError::ProfileNotUTF8)? .into_owned(), ), None => None, }; let comment = match start.get_attribute("comment")? { Some(comment_attr) => Some( comment_attr .value() .map_err(|_| RequirementError::CommentNotUTF8)? .into_owned(), ), None => None, }; let features = deserializer.de_tag_list::(None::<&str>)?; 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, /// Invalid feature. #[error("Invalid feature")] InvalidFeature(#[from] FeatureError), /// Deserialization failed. #[error("Deserialization failed")] DeserializationFailed(#[from] DeserializationError), /// Invalid tag start. #[error("Invalid tag start")] InvalidTagStart(#[from] TagStartError), } impl_from_deserializer_err_wrapped!(RequirementError); impl_from_deserializer_error!(RequirementError); /// 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 DeserializeTagged for Removal { type Error = RemovalError; fn deserialize( start: &TagStart, deserializer: &mut TDeserializer, ) -> Result { let profile = start .get_attribute("profile")? .ok_or_else(|| RemovalError::MissingProfile)? .value() .map_err(|_| RemovalError::ProfileNotUTF8)? .into_owned(); let comment = start .get_attribute("comment")? .ok_or_else(|| RemovalError::MissingComment)? .value() .map_err(|_| RemovalError::CommentNotUTF8)? .into_owned(); let features = deserializer.de_tag_list::(None::<&str>)?; 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, /// Invalid feature. #[error("Invalid feature")] InvalidFeature(#[from] FeatureError), /// Deserialization failed. #[error("Deserialization failed")] DeserializationFailed(#[from] DeserializationError), /// Invalid tag start. #[error("Invalid tag start")] InvalidTagStart(#[from] TagStartError), } impl_from_deserializer_err_wrapped!(RemovalError); impl_from_deserializer_error!(RemovalError); /// 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 DeserializeTagged for Feature { type Error = FeatureError; fn deserialize( start: &TagStart, _deserializer: &mut TDeserializer, ) -> Result { let kind = match start.name_bytes() { 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 = start .get_attribute("name")? .ok_or_else(|| FeatureError::MissingName)? .value() .map_err(|_| FeatureError::NameNotUTF8)? .into_owned(); let comment = start .get_attribute("comment")? .map(|comment_attr| { comment_attr .value() .map(Cow::into_owned) .map_err(|_| FeatureError::CommentNotUTF8) }) .transpose()?; 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), /// Invalid tag start. #[error("Invalid tag start")] InvalidTagStart(#[from] TagStartError), } impl_from_deserializer_err_wrapped!(FeatureError); impl_from_deserializer_error!(FeatureError); /// 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 xml_stinks::attribute::Attribute; use super::*; use crate::test_utils::MockDeserializer; #[test] fn deserialize_feature_works() { let mut mock_deserializer = MockDeserializer::new(); let commentless_enum_feature = Feature::deserialize( &TagStart::new("enum") .with_attributes([Attribute::new("name", "GL_BLEND".as_bytes())]), &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( &TagStart::new("enum").with_attributes([ Attribute::new("name", "GL_VERTEX_BINDING_BUFFER".as_bytes()), Attribute::new( "comment", "Added in 2013/10/22 update to the spec".as_bytes(), ), ]), &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( &TagStart::new("command") .with_attributes([Attribute::new("name", "glCreateBuffers".as_bytes())]), &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( &TagStart::new("type") .with_attributes([Attribute::new("name", "GLbyte".as_bytes())]), &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(&TagStart::new("foo"), &mut mock_deserializer), Err(FeatureError::UnknownFeatureKind(_)) )); } #[test] fn deserialize_removal_works() { let mut mock_deserializer = MockDeserializer::new(); mock_deserializer .expect_de_tag_list::<_, &str>() .times(1) .returning(|_| { Ok(vec![ Feature::new(FeatureKind::Command, "glNewList", None), Feature::new(FeatureKind::Command, "glEndList", None), ]) }); let removal = Removal::deserialize( &TagStart::new("remove").with_attributes([ Attribute::new("profile", "core".as_bytes()), Attribute::new( "comment", "Compatibility-only GL 1.0 features removed from GL 3.2".as_bytes(), ), ]), &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_tag_list::<_, &str>() .times(1) .returning(|_| { Ok(vec![ Feature::new(FeatureKind::Command, "glNewList", None), Feature::new(FeatureKind::Command, "glEndList", None), ]) }); mock_deserializer .expect_de_tag_list::<_, &str>() .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_tag_list::<_, &str>() .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(&TagStart::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( &TagStart::new("require") .with_attributes([Attribute::new("profile", "compatibility".as_bytes())]), &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( &TagStart::new("require").with_attributes([ Attribute::new("profile", "core".as_bytes()), Attribute::new( "comment", concat!( "Restore functionality removed in GL 3.2 core ", "to GL 4.3. Needed for debug interface." ) .as_bytes(), ), ]), &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::<_, &str>() .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::<_, &str>() .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( &TagStart::new("feature").with_attributes([ Attribute::new("api", "gl".as_bytes()), Attribute::new("name", "GL_VERSION_3_2".as_bytes()), Attribute::new("number", "3.2".as_bytes()), ]), &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); } }