use std::num::{ParseFloatError, ParseIntError}; macro_rules! keyword { ( $(#[$attr: meta])* $visibility: vis enum $name: ident { $( $(#[keyword(rename = $variant_rename: literal)])? $variant: ident, )* } ) => { $(#[$attr])* $visibility enum $name { $($variant,)* } impl $crate::file_format::wavefront::common::Keyword for $name { fn from_str( keyword: &str, line_no: usize ) -> Result { use $crate::file_format::wavefront::common::ParsingError; match keyword { $( $crate::util::or!( ($($variant_rename)?) else (stringify!($variant)) ) => Ok(Keyword::$variant),)* _ => Err(ParsingError::UnknownKeyword { keyword: keyword.to_string(), line_no, }), } } } impl std::fmt::Display for $name { fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { $(Self::$variant => write!(formatter, stringify!($variant)),)* } } } }; } pub(crate) use keyword; pub fn parse_statement_line( line: &str, line_no: usize, ) -> Result>, ParsingError> where KeywordT: Keyword, { if line.is_empty() || line.starts_with('#') { return Ok(None); } let mut parts = line.split(' '); let keyword = KeywordT::from_str(parts.next().unwrap(), line_no)?; let arguments = parts .map(|part| Value::parse::(part, line_no)) .collect::, _>>()?; Ok(Some(Statement { keyword, arguments })) } #[derive(Debug, thiserror::Error)] pub enum ParsingError { #[error("Unknown keyword '{keyword}' at line {line_no}")] UnknownKeyword { keyword: String, line_no: usize }, #[error("Invalid float value '{value}'")] InvalidFloatValue { err: ParseFloatError, value: String }, #[error("Invalid integer value '{value}' at line {line_no}")] InvalidIntegerValue { err: ParseIntError, value: String, line_no: usize, }, #[error("Invalid type of value '{0}'")] UnknownValueType(String), #[error("Missing triplet value at line {line_no}")] MissingTripletValue { triplet_index: usize, line_no: usize, }, #[error("Missing argument to {keyword} at {line_no}")] MissingArgument { keyword: String, line_no: usize }, #[error("Unexpected type of argument {argument_index} at line {line_no}")] UnexpectedArgumentType { argument_index: usize, line_no: usize, }, } pub trait Keyword: ToString + Sized { fn from_str(keyword: &str, line_no: usize) -> Result; } type TripletNumber = u32; #[derive(Debug, Clone)] pub struct Triplet( pub TripletNumber, pub Option, pub Option, ); impl Triplet { fn parse( triplet: &str, line_no: usize, ) -> Result { let mut parts = triplet.splitn(3, '/'); let first = parts .next() .ok_or(ParsingError::MissingTripletValue { triplet_index: 0, line_no }) .and_then(|value| { value.parse::().map_err(|err| { ParsingError::InvalidIntegerValue { err, value: value.to_string(), line_no, } }) })?; let second = parts .next() .ok_or(ParsingError::MissingTripletValue { triplet_index: 0, line_no }) .and_then(|value| { if value.is_empty() { return Ok(None); } Ok(Some(value.parse::().map_err(|err| { ParsingError::InvalidIntegerValue { err, value: value.to_string(), line_no, } })?)) })?; let third = parts .next() .ok_or(ParsingError::MissingTripletValue { triplet_index: 2, line_no }) .and_then(|value| { if value.is_empty() { return Ok(None); } Ok(Some(value.parse::().map_err(|err| { ParsingError::InvalidIntegerValue { err, value: value.to_string(), line_no, } })?)) })?; Ok(Self(first, second, third)) } } #[derive(Debug)] pub enum Value { #[allow(dead_code)] Integer(i32), Float(f32), Triplet(Triplet), Text(String), } impl Value { fn parse(value: &str, line_no: usize) -> Result { if value.contains(|character: char| character.is_ascii_digit()) && value.contains('.') { return Ok(Self::Float(value.parse().map_err(|err| { ParsingError::InvalidFloatValue { err, value: value.to_string() } })?)); } if value.contains('/') && value .chars() .all(|character| character.is_ascii_digit() || character == '/') { return Ok(Self::Triplet(Triplet::parse::(value, line_no)?)); } if !value.is_empty() && value .chars() .all(|character: char| character.is_ascii_digit()) { return Ok(Self::Integer(value.parse().map_err(|err| { ParsingError::InvalidIntegerValue { err, value: value.to_string(), line_no, } })?)); } if value.chars().all(|character| character.is_ascii()) { return Ok(Self::Text(value.to_string())); } Err(ParsingError::UnknownValueType(value.to_string())) } pub fn to_text( &self, argument_index: usize, line_no: usize, ) -> Result<&str, ParsingError> { match self { Value::Text(value) => Ok(value), _ => Err(ParsingError::UnexpectedArgumentType { argument_index, line_no }), } } } #[derive(Debug)] pub struct Statement { pub keyword: KeywordT, pub arguments: Vec, } impl Statement { pub fn get_float_arg(&self, index: usize, line_no: usize) -> Result { match self.arguments.get(index) { Some(Value::Float(value)) => Ok(*value), Some(_) => Err(ParsingError::UnexpectedArgumentType { argument_index: index, line_no, }), None => Err(ParsingError::MissingArgument { keyword: self.keyword.to_string(), line_no, }), } } pub fn get_text_arg(&self, index: usize, line_no: usize) -> Result<&str, ParsingError> { match self.arguments.get(index) { Some(Value::Text(value)) => Ok(value), Some(_) => Err(ParsingError::UnexpectedArgumentType { argument_index: index, line_no, }), None => Err(ParsingError::MissingArgument { keyword: self.keyword.to_string(), line_no, }), } } pub fn get_triplet_arg( &self, index: usize, line_no: usize, ) -> Result { match self.arguments.get(index) { Some(Value::Triplet(value)) => Ok(value.clone()), Some(_) => Err(ParsingError::UnexpectedArgumentType { argument_index: index, line_no, }), None => Err(ParsingError::MissingArgument { keyword: self.keyword.to_string(), line_no, }), } } } #[cfg(test)] mod tests { use std::fmt::Display; use super::{parse_statement_line, Value}; use crate::file_format::wavefront::common::ParsingError; #[derive(Debug, PartialEq, Eq)] enum Keyword { V, Vn, F, } impl super::Keyword for Keyword { fn from_str(keyword: &str, _line_no: usize) -> Result { Ok(match keyword { "v" => Self::V, "vn" => Self::Vn, "f" => Self::F, _ => { panic!("Unknown keyword '{keyword}'"); } }) } } impl Display for Keyword { fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::V => write!(formatter, "v"), Self::Vn => write!(formatter, "vn"), Self::F => write!(formatter, "f"), } } } #[test] fn parse_statement_line_works() { assert!(parse_statement_line::("", 1) .is_ok_and(|statement| statement.is_none())); let res = parse_statement_line::("v", 3); assert!(matches!(res, Ok(Some(statement)) if statement.keyword == Keyword::V)); } #[test] fn parse_statement_line_with_args_works() { let statement = parse_statement_line::("vn 45 86 11", 3) .expect("Expected Ok") .expect("Expected Some"); assert_eq!(statement.keyword, Keyword::Vn); assert!(matches!(statement.arguments[0], Value::Integer(45))); assert!(matches!(statement.arguments[1], Value::Integer(86))); assert!(matches!(statement.arguments[2], Value::Integer(11))); } #[test] fn parse_statement_line_with_triplet_arg() { let statement = parse_statement_line::("f 67/925/23", 1) .expect("Expected Ok") .expect("Expected Some"); assert_eq!(statement.keyword, Keyword::F); let Value::Triplet(triplet) = &statement.arguments[0] else { panic!("Argument is not a triplet"); }; assert_eq!(triplet.0, 67); assert_eq!(triplet.1, Some(925)); assert_eq!(triplet.2, Some(23)); } #[test] fn parse_statement_line_with_two_value_triplet_arg() { let statement = parse_statement_line::("v 102//336", 1) .expect("Expected Ok") .expect("Expected Some"); assert_eq!(statement.keyword, Keyword::V); let Value::Triplet(triplet) = &statement.arguments[0] else { panic!("Argument is not a triplet"); }; assert_eq!(triplet.0, 102); assert!(triplet.1.is_none()); assert_eq!(triplet.2, Some(336)); } }