use chumsky::{error::Rich, Parser as ChumskyParser};
use crate::parser::{any_quoted_string, quoted_string, TokenError, TokenSource};
#[derive(Debug, Default, Copy, Clone, PartialEq)]
pub struct Point3D {
pub x: f32,
pub y: f32,
pub z: f32,
}
pub(crate) fn key_value_point3d<'src, I>(
key: &'src str,
) -> impl ChumskyParser<'src, I, Point3D, TokenError<'src>>
where
I: TokenSource<'src>,
{
use chumsky::error::Rich;
quoted_string(key)
.ignore_then(any_quoted_string())
.try_map(move |value_str, span| {
parse_point_from_numbers_str(value_str)
.map_err(|err_msg| Rich::custom(span, format!("Invalid point: {}", err_msg)))
})
}
pub(crate) fn parse_point_from_numbers_str(numbers_str: &str) -> Result<Point3D, String> {
let mut parts = numbers_str.split_whitespace();
if let (Some(x), Some(y), Some(z)) = (parts.next(), parts.next(), parts.next()) {
let x = x
.parse::<f32>()
.map_err(|e| format!("invalid x '{}': {}", x, e))?;
let y = y
.parse::<f32>()
.map_err(|e| format!("invalid y '{}': {}", y, e))?;
let z = z
.parse::<f32>()
.map_err(|e| format!("invalid z '{}': {}", z, e))?;
Ok(Point3D { x, y, z })
} else {
Err("invalid number of parts".to_string())
}
}
pub(crate) fn key_value_plane<'src, I>(
key: &'static str,
) -> impl ChumskyParser<'src, I, (Point3D, Point3D, Point3D), TokenError<'src>>
where
I: TokenSource<'src>,
{
quoted_string(key)
.ignore_then(any_quoted_string())
.try_map(move |plane_value_str, span| {
let mut points = [Point3D::default(); 3];
let mut remainder = plane_value_str.trim();
for i in 0..3 {
if let Some(open_idx) = remainder.find('(') {
remainder = &remainder[open_idx + 1..];
} else {
return Err(Rich::custom(
span,
format!("Point {}: missing opening parenthesis", i + 1),
));
}
if let Some(close_idx) = remainder.find(')') {
let numbers_part = &remainder[..close_idx];
remainder = &remainder[close_idx + 1..];
match parse_point_from_numbers_str(numbers_part) {
Ok(point) => points[i] = point,
Err(err_msg) => {
return Err(Rich::custom(
span,
format!("Point {}: {} (in '{}')", i + 1, err_msg, numbers_part),
));
}
}
} else {
return Err(Rich::custom(
span,
format!("Point {}: missing closing parenthesis", i + 1),
));
}
}
if points.len() == 3 {
Ok((points[0], points[1], points[2]))
} else {
Err(Rich::custom(
span,
"Internal error: Failed to collect 3 points".to_string(),
))
}
})
}
#[cfg(test)]
mod tests {
use chumsky::Parser as _;
use crate::{
types::point::{key_value_plane, Point3D},
util::lex,
};
#[test]
fn test_parse_valid_plane() {
let stream = lex(r#""test_plane" "(1.0 2.0 3.0) (4.0 5.0 6.0) (7.0 8.0 9.0)""#);
let parser = key_value_plane("test_plane");
let result = parser.parse(stream).into_result();
assert!(result.is_ok(), "Parsing failed: {:?}", result.err());
let (p1, p2, p3) = result.unwrap();
assert_eq!(
p1,
Point3D {
x: 1.0,
y: 2.0,
z: 3.0
}
);
assert_eq!(
p2,
Point3D {
x: 4.0,
y: 5.0,
z: 6.0
}
);
assert_eq!(
p3,
Point3D {
x: 7.0,
y: 8.0,
z: 9.0
}
);
}
#[test]
fn test_parse_plane_malformed_numbers() {
let stream = lex(r#""test_plane" "(1.0 2.0 oops) (4.0 5.0 6.0) (7.0 8.0 9.0)""#);
let parser = key_value_plane("test_plane");
let result = parser.parse(stream).into_result();
assert!(result.is_err());
}
#[test]
fn test_parse_plane_missing_paren() {
let stream = lex(r#""test_plane" "(1.0 2.0 3.0 (4.0 5.0 6.0) (7.0 8.0 9.0)""#);
let parser = key_value_plane("test_plane");
let result = parser.parse(stream).into_result();
assert!(result.is_err());
}
#[test]
fn test_parse_plane_too_few_points() {
let stream = lex(r#""test_plane" "(1.0 2.0 3.0) (4.0 5.0 6.0)""#);
let parser = key_value_plane("test_plane");
let result = parser.parse(stream).into_result();
assert!(result.is_err());
}
}