#![forbid(unsafe_code)]
use super::{
tds::CellKey,
traits::DataType,
util::{UuidValidationError, make_uuid, validate_uuid},
};
use crate::geometry::{
point::Point,
traits::coordinate::{Coordinate, CoordinateScalar, CoordinateValidationError},
};
use serde::{
Deserialize, Serialize,
de::{self, IgnoredAny, MapAccess, Visitor},
};
use std::{
cmp::Ordering,
collections::HashMap,
fmt::{self, Debug},
hash::{Hash, Hasher},
marker::PhantomData,
};
use thiserror::Error;
use uuid::Uuid;
#[derive(Clone, Debug, Error, PartialEq, Eq)]
pub enum VertexValidationError {
#[error("Invalid point: {source}")]
InvalidPoint {
#[from]
source: CoordinateValidationError,
},
#[error("Invalid UUID: {source}")]
InvalidUuid {
#[from]
source: UuidValidationError,
},
}
#[derive(Clone, Debug, Error, PartialEq, Eq)]
pub enum VertexBuilderError {
#[error("Missing required field: `point`")]
MissingPoint,
}
pub struct VertexBuilder<T, U, const D: usize> {
point: Option<Point<T, D>>,
data: Option<U>,
}
impl<T, U, const D: usize> Default for VertexBuilder<T, U, D>
where
U: DataType,
{
fn default() -> Self {
Self {
point: None,
data: None,
}
}
}
impl<T, U, const D: usize> VertexBuilder<T, U, D>
where
U: DataType,
{
#[must_use]
pub fn point(mut self, point: Point<T, D>) -> Self {
self.point = Some(point);
self
}
#[must_use]
pub fn data(mut self, data: impl Into<U>) -> Self {
self.data = Some(data.into());
self
}
pub fn build(self) -> Result<Vertex<T, U, D>, VertexBuilderError> {
let point = self.point.ok_or(VertexBuilderError::MissingPoint)?;
Ok(Vertex {
point,
uuid: make_uuid(),
incident_cell: None,
data: self.data,
})
}
}
#[macro_export]
macro_rules! vertex {
($coords:expr) => {
$crate::core::vertex::VertexBuilder::<_, (), _>::default()
.point($crate::geometry::point::Point::try_from($coords)
.expect("Failed to convert coordinates to Point: invalid or out-of-range values"))
.build()
.expect("Failed to build vertex: invalid coordinates or builder configuration")
};
($coords:expr, $data:expr) => {
$crate::core::vertex::VertexBuilder::default()
.point($crate::geometry::point::Point::try_from($coords)
.expect("Failed to convert coordinates to Point: invalid or out-of-range values"))
.data($data)
.build()
.expect("Failed to build vertex with data: invalid coordinates, data, or builder configuration")
};
}
pub use crate::vertex;
#[derive(Clone, Copy, Debug)]
pub struct Vertex<T, U, const D: usize> {
point: Point<T, D>,
uuid: Uuid,
pub incident_cell: Option<CellKey>,
pub(crate) data: Option<U>,
}
impl<T, U, const D: usize> Vertex<T, U, D>
where
U: DataType,
{
#[inline]
pub const fn uuid(&self) -> Uuid {
self.uuid
}
#[inline]
pub const fn dim(&self) -> usize {
D
}
#[inline]
pub const fn point(&self) -> &Point<T, D> {
&self.point
}
#[inline]
#[must_use]
pub const fn data(&self) -> Option<&U> {
self.data.as_ref()
}
}
impl<T, U, const D: usize> Serialize for Vertex<T, U, D>
where
T: CoordinateScalar,
U: DataType,
{
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
use serde::ser::SerializeStruct;
let field_count = if self.data.is_some() { 3 } else { 2 };
let mut state = serializer.serialize_struct("Vertex", field_count)?;
state.serialize_field("point", &self.point)?;
state.serialize_field("uuid", &self.uuid)?;
if self.data.is_some() {
state.serialize_field("data", &self.data)?;
}
state.end()
}
}
impl<'de, T, U, const D: usize> Deserialize<'de> for Vertex<T, U, D>
where
T: CoordinateScalar,
U: DataType,
{
fn deserialize<De>(deserializer: De) -> Result<Self, De::Error>
where
De: serde::Deserializer<'de>,
{
struct VertexVisitor<T, U, const D: usize>
where
T: CoordinateScalar,
U: DataType,
{
_phantom: PhantomData<(T, U)>,
}
impl<'de, T, U, const D: usize> Visitor<'de> for VertexVisitor<T, U, D>
where
T: CoordinateScalar,
U: DataType,
{
type Value = Vertex<T, U, D>;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a Vertex struct")
}
fn visit_map<V>(self, mut map: V) -> Result<Vertex<T, U, D>, V::Error>
where
V: MapAccess<'de>,
{
let mut point: Option<Point<T, D>> = None;
let mut uuid = None;
let mut incident_cell = None;
let mut data = None;
while let Some(key) = map.next_key()? {
match key {
"point" => {
if point.is_some() {
return Err(de::Error::duplicate_field("point"));
}
point = Some(map.next_value()?);
}
"uuid" => {
if uuid.is_some() {
return Err(de::Error::duplicate_field("uuid"));
}
uuid = Some(map.next_value()?);
}
"incident_cell" => {
if incident_cell.is_some() {
return Err(de::Error::duplicate_field("incident_cell"));
}
let _ = map.next_value::<IgnoredAny>()?;
incident_cell = Some(None);
}
"data" => {
if data.is_some() {
return Err(de::Error::duplicate_field("data"));
}
data = Some(map.next_value()?);
}
_ => {
let _ = map.next_value::<IgnoredAny>()?;
}
}
}
let point = point.ok_or_else(|| de::Error::missing_field("point"))?;
let uuid: Uuid = uuid.ok_or_else(|| de::Error::missing_field("uuid"))?;
validate_uuid(&uuid)
.map_err(|e| de::Error::custom(format!("invalid uuid: {e}")))?;
let incident_cell = incident_cell.unwrap_or(None);
let data = data.unwrap_or(None);
point.validate().map_err(|e| {
de::Error::custom(format!("Invalid point during deserialization: {e}"))
})?;
Ok(Vertex {
point,
uuid,
incident_cell,
data,
})
}
}
const FIELDS: &[&str] = &["point", "uuid", "incident_cell", "data"];
deserializer.deserialize_struct(
"Vertex",
FIELDS,
VertexVisitor {
_phantom: PhantomData,
},
)
}
}
impl<T, U, const D: usize> Vertex<T, U, D>
where
T: CoordinateScalar,
U: DataType,
{
#[must_use]
pub fn empty() -> Self
where
T: Default,
{
Self {
point: Point::default(),
uuid: Uuid::nil(),
incident_cell: None,
data: None,
}
}
#[inline]
#[must_use]
pub fn from_points(points: &[Point<T, D>]) -> Vec<Self> {
points
.iter()
.map(|p| VertexBuilder::default().point(*p).build().unwrap())
.collect()
}
#[inline]
#[must_use]
pub fn into_hashmap<I>(vertices: I) -> HashMap<Uuid, Self>
where
I: IntoIterator<Item = Self>,
{
vertices.into_iter().map(|v| (v.uuid(), v)).collect()
}
#[cfg(test)]
pub(crate) fn set_uuid(&mut self, uuid: Uuid) -> Result<(), VertexValidationError> {
validate_uuid(&uuid)?;
self.uuid = uuid;
Ok(())
}
pub fn is_valid(self) -> Result<(), VertexValidationError>
where
Point<T, D>: Coordinate<T, D>,
{
self.point
.validate()
.map_err(|source| VertexValidationError::InvalidPoint { source })?;
validate_uuid(&self.uuid())?;
Ok(())
}
#[doc(hidden)]
pub const fn new_with_uuid(point: Point<T, D>, uuid: Uuid, data: Option<U>) -> Self {
Self {
point,
uuid,
incident_cell: None,
data,
}
}
}
impl<T, U, const D: usize> PartialEq for Vertex<T, U, D>
where
T: CoordinateScalar,
U: DataType,
{
#[inline]
fn eq(&self, other: &Self) -> bool {
self.point.ordered_equals(&other.point)
}
}
impl<T, U, const D: usize> PartialOrd for Vertex<T, U, D>
where
T: CoordinateScalar,
U: DataType,
{
#[inline]
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
self.point.partial_cmp(&other.point)
}
}
impl<T, U, const D: usize> From<Vertex<T, U, D>> for [T; D]
where
T: CoordinateScalar,
U: DataType,
{
#[inline]
fn from(vertex: Vertex<T, U, D>) -> [T; D] {
*vertex.point().coords()
}
}
impl<T, U, const D: usize> From<&Vertex<T, U, D>> for [T; D]
where
T: CoordinateScalar,
U: DataType,
{
#[inline]
fn from(vertex: &Vertex<T, U, D>) -> [T; D] {
*vertex.point().coords()
}
}
impl<T, U, const D: usize> From<&Vertex<T, U, D>> for Point<T, D>
where
T: CoordinateScalar,
U: DataType,
{
#[inline]
fn from(vertex: &Vertex<T, U, D>) -> Self {
*vertex.point()
}
}
impl<T, U, const D: usize> Eq for Vertex<T, U, D>
where
T: CoordinateScalar,
U: DataType,
{
}
impl<T, U, const D: usize> Hash for Vertex<T, U, D>
where
T: CoordinateScalar,
U: DataType,
Point<T, D>: Hash,
{
fn hash<H: Hasher>(&self, state: &mut H) {
self.point.hash_coordinate(state);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::collections::{FastHashMap, FastHashSet};
use crate::core::tds::CellKey;
use crate::core::util::{UuidValidationError, make_uuid, usize_to_u8};
use crate::geometry::point::Point;
use crate::geometry::traits::coordinate::Coordinate;
use approx::{assert_abs_diff_eq, assert_relative_eq};
use serde::{Deserialize, Serialize};
use slotmap::KeyData;
use std::collections::hash_map::DefaultHasher;
use std::hash::Hasher;
#[repr(u8)]
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
enum PointType {
Origin = 0,
Boundary = 1,
Interior = 2,
Corner = 3,
}
impl From<PointType> for u8 {
fn from(point_type: PointType) -> Self {
point_type as Self
}
}
impl Serialize for PointType {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_u8(u8::from(*self))
}
}
impl<'de> Deserialize<'de> for PointType {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = u8::deserialize(deserializer)?;
match value {
0 => Ok(Self::Origin),
1 => Ok(Self::Boundary),
2 => Ok(Self::Interior),
3 => Ok(Self::Corner),
_ => Err(serde::de::Error::custom(format!(
"Invalid PointType: {value}"
))),
}
}
}
fn assert_vertex_properties<T, U, const D: usize>(
vertex: &Vertex<T, U, D>,
expected_coords: [T; D],
) where
T: CoordinateScalar,
U: DataType,
{
assert_eq!(vertex.point().coords(), &expected_coords);
assert_eq!(vertex.dim(), D);
assert!(!vertex.uuid().is_nil());
assert!(vertex.incident_cell.is_none());
}
#[test]
fn test_vertex_builder_with_point() {
let v: Vertex<f64, (), 3> = VertexBuilder::default()
.point(Point::new([1.0, 2.0, 3.0]))
.build()
.unwrap();
assert_relative_eq!(
v.point().coords().as_slice(),
[1.0, 2.0, 3.0].as_slice(),
epsilon = 1e-9
);
assert!(!v.uuid().is_nil());
assert!(v.incident_cell.is_none());
assert!(v.data.is_none());
}
#[test]
fn test_vertex_builder_with_data() {
let v: Vertex<f64, i32, 2> = VertexBuilder::default()
.point(Point::new([0.0, 1.0]))
.data(42)
.build()
.unwrap();
assert_relative_eq!(
v.point().coords().as_slice(),
[0.0, 1.0].as_slice(),
epsilon = 1e-9
);
assert_eq!(v.data, Some(42));
}
#[test]
fn test_vertex_builder_missing_point() {
let result = VertexBuilder::<f64, (), 3>::default().build();
assert_eq!(result, Err(VertexBuilderError::MissingPoint));
}
#[test]
fn test_vertex_data_accessor() {
let v_with: Vertex<f64, i32, 2> = vertex!([1.0, 2.0], 42);
assert_eq!(v_with.data(), Some(&42));
let v_without: Vertex<f64, (), 2> = vertex!([1.0, 2.0]);
assert_eq!(v_without.data(), None);
}
#[test]
fn test_vertex_macro() {
let v1: Vertex<f64, (), 3> = vertex!([1.0, 2.0, 3.0]);
assert_relative_eq!(
v1.point().coords().as_slice(),
[1.0, 2.0, 3.0].as_slice(),
epsilon = 1e-9
);
assert_eq!(v1.dim(), 3);
assert!(!v1.uuid().is_nil());
assert!(v1.data.is_none());
let v2: Vertex<f64, i32, 2> = vertex!([0.0, 1.0], 99);
assert_relative_eq!(
v2.point().coords().as_slice(),
[0.0, 1.0].as_slice(),
epsilon = 1e-9
);
assert_eq!(v2.dim(), 2);
assert!(!v2.uuid().is_nil());
assert_eq!(v2.data.unwrap(), 99);
let v3: Vertex<f64, u32, 4> = vertex!([1.0, 2.0, 3.0, 4.0], 42u32);
assert_relative_eq!(
v3.point().coords().as_slice(),
[1.0f64, 2.0f64, 3.0f64, 4.0f64].as_slice(),
epsilon = 1e-9
);
assert_eq!(v3.dim(), 4);
assert_eq!(v3.data.unwrap(), 42u32);
}
#[test]
fn test_pointtype_u8_conversion() {
assert_eq!(u8::from(PointType::Origin), 0);
assert_eq!(u8::from(PointType::Boundary), 1);
assert_eq!(u8::from(PointType::Interior), 2);
assert_eq!(u8::from(PointType::Corner), 3);
let origin_json = serde_json::to_string(&PointType::Origin).unwrap();
assert_eq!(origin_json, "0");
let corner_json = serde_json::to_string(&PointType::Corner).unwrap();
assert_eq!(corner_json, "3");
let original = PointType::Boundary;
let serialized = serde_json::to_string(&original).unwrap();
let deserialized: PointType = serde_json::from_str(&serialized).unwrap();
assert_eq!(original, deserialized);
println!("✓ PointType u8::from conversion works correctly");
}
#[test]
fn test_vertex_basic_operations() {
let empty_vertex: Vertex<f64, (), 3> = Vertex::empty();
assert_relative_eq!(
empty_vertex.point().coords().as_slice(),
[0.0, 0.0, 0.0].as_slice(),
epsilon = 1e-9
);
assert_eq!(empty_vertex.dim(), 3);
assert!(empty_vertex.uuid().is_nil());
assert!(empty_vertex.incident_cell.is_none());
assert!(empty_vertex.data.is_none());
let vertex: Vertex<f64, u8, 4> = vertex!([1.0, 2.0, 3.0, 4.0], 4u8);
let vertex_copy = vertex;
assert_eq!(vertex, vertex_copy);
assert_relative_eq!(
vertex_copy.point().coords().as_slice(),
[1.0, 2.0, 3.0, 4.0].as_slice(),
epsilon = 1e-9
);
let points = vec![
Point::new([1.0, 2.0, 3.0]),
Point::new([4.0, 5.0, 6.0]),
Point::new([7.0, 8.0, 9.0]),
];
let mut vertices: Vec<Vertex<f64, (), 3>> = Vertex::from_points(&points);
assert_eq!(vertices.len(), 3);
assert_relative_eq!(
vertices[0].point().coords().as_slice(),
[1.0, 2.0, 3.0].as_slice(),
epsilon = 1e-9
);
assert_eq!(vertices[0].dim(), 3);
assert_relative_eq!(
vertices[1].point().coords().as_slice(),
[4.0, 5.0, 6.0].as_slice(),
epsilon = 1e-9
);
assert_eq!(vertices[1].dim(), 3);
assert_relative_eq!(
vertices[2].point().coords().as_slice(),
[7.0, 8.0, 9.0].as_slice(),
epsilon = 1e-9
);
assert_eq!(vertices[2].dim(), 3);
let empty_points: Vec<Point<f64, 3>> = Vec::new();
let empty_vertices: Vec<Vertex<f64, (), 3>> = Vertex::from_points(&empty_points);
assert!(empty_vertices.is_empty());
let single_point = vec![Point::new([1.0, 2.0, 3.0])];
let single_vertices: Vec<Vertex<f64, (), 3>> = Vertex::from_points(&single_point);
assert_eq!(single_vertices.len(), 1);
assert_relative_eq!(
single_vertices[0].point().coords().as_slice(),
[1.0, 2.0, 3.0].as_slice(),
epsilon = 1e-9
);
assert_eq!(single_vertices[0].dim(), 3);
assert!(!single_vertices[0].uuid().is_nil());
let hashmap = Vertex::into_hashmap(vertices.iter().copied());
let mut values: Vec<Vertex<f64, (), 3>> = hashmap.into_values().collect();
assert_eq!(values.len(), 3);
values.sort_by_key(super::Vertex::uuid);
vertices.sort_by_key(super::Vertex::uuid);
assert_eq!(values, vertices);
let empty_hashmap = Vertex::<f64, (), 3>::into_hashmap([]);
assert!(empty_hashmap.is_empty());
let single_vertex: Vertex<f64, (), 3> = vertex!([1.0, 2.0, 3.0]);
let uuid = single_vertex.uuid();
let single_hashmap = Vertex::into_hashmap([single_vertex]);
assert_eq!(single_hashmap.len(), 1);
assert!(single_hashmap.contains_key(&uuid));
assert_relative_eq!(
single_hashmap
.get(&uuid)
.unwrap()
.point()
.coords()
.as_slice(),
[1.0, 2.0, 3.0].as_slice(),
epsilon = 1e-9
);
println!("{empty_vertex:?}");
println!("{vertices:?}");
println!("values = {values:?}");
}
#[test]
fn test_vertex_serialization_roundtrip() {
let vertex: Vertex<f64, (), 3> = vertex!([1.0, 2.0, 3.0]);
let serialized = serde_json::to_string(&vertex).unwrap();
assert!(serialized.contains("point"));
assert!(serialized.contains("[1.0,2.0,3.0]"));
let deserialized: Vertex<f64, (), 3> = serde_json::from_str(&serialized).unwrap();
assert_relative_eq!(
deserialized.point().coords().as_slice(),
vertex.point().coords().as_slice(),
epsilon = f64::EPSILON
);
assert_eq!(deserialized.dim(), vertex.dim());
assert_eq!(deserialized.incident_cell, vertex.incident_cell);
assert_eq!(deserialized.data, vertex.data);
assert_eq!(deserialized.uuid(), vertex.uuid());
let vertex_with_data: Vertex<f64, i32, 3> = vertex!([1.0, 2.0, 3.0], 42);
let serialized_with_data = serde_json::to_string(&vertex_with_data).unwrap();
assert!(serialized_with_data.contains("\"data\":"));
assert!(serialized_with_data.contains("42"));
let deserialized_with_data: Vertex<f64, i32, 3> =
serde_json::from_str(&serialized_with_data).unwrap();
assert_eq!(deserialized_with_data.data, Some(42));
assert_relative_eq!(
deserialized_with_data.point().coords().as_slice(),
[1.0, 2.0, 3.0].as_slice(),
epsilon = f64::EPSILON
);
let vertex_no_data: Vertex<f64, (), 3> = vertex!([1.0, 2.0, 3.0]);
let serialized_no_data = serde_json::to_string(&vertex_no_data).unwrap();
assert!(!serialized_no_data.contains("\"data\":"));
let deserialized_no_data: Vertex<f64, (), 3> =
serde_json::from_str(&serialized_no_data).unwrap();
assert_eq!(deserialized_no_data.data, None);
let json_with_null =
r#"{"point":[1.0,2.0,3.0],"uuid":"550e8400-e29b-41d4-a716-446655440000","data":null}"#;
let vertex_null_data: Vertex<f64, (), 3> = serde_json::from_str(json_with_null).unwrap();
assert_eq!(vertex_null_data.data, None);
let vertex_char: Vertex<f64, char, 4> = vertex!([1.0, 2.0, 3.0, 4.0], 'A');
let serialized_char = serde_json::to_string(&vertex_char).unwrap();
let deserialized_char: Vertex<f64, char, 4> =
serde_json::from_str(&serialized_char).unwrap();
assert_eq!(deserialized_char.data, Some('A'));
assert_relative_eq!(
deserialized_char.point().coords().as_slice(),
[1.0, 2.0, 3.0, 4.0].as_slice(),
epsilon = f64::EPSILON
);
println!("Serialized: {serialized:?}");
}
#[test]
fn test_vertex_equality_and_hashing() {
let v1: Vertex<f64, (), 3> = vertex!([1.0, 2.0, 3.0]);
let v2: Vertex<f64, (), 3> = vertex!([1.0, 2.0, 3.0]);
let v3: Vertex<f64, (), 3> = vertex!([1.0, 2.0, 4.0]);
assert_eq!(v1, v2);
assert!(v1.eq(&v2));
assert!(v2.eq(&v1));
assert_ne!(v1, v3);
assert_ne!(v2, v3);
assert!(!v1.eq(&v3));
assert!(!v2.eq(&v3));
assert_eq!(v1, v1);
assert!(v1.eq(&v1));
let v4: Vertex<f64, i32, 2> = vertex!([1.0, 2.0], 42);
let v5: Vertex<f64, i32, 2> = vertex!([1.0, 2.0], 99);
assert_ne!(v4.uuid(), v5.uuid());
assert_ne!(v4.data, v5.data);
assert_eq!(v4, v5);
let v6: Vertex<f64, (), 2> = vertex!([1.0, 2.0]);
let v7: Vertex<f64, (), 2> = vertex!([1.0, 2.0]);
assert_eq!(v6, v7);
let mut hasher1 = DefaultHasher::new();
let mut hasher2 = DefaultHasher::new();
v1.hash(&mut hasher1);
v1.hash(&mut hasher2);
let hash1 = hasher1.finish();
let hash2 = hasher2.finish();
assert_eq!(hash1, hash2);
let mut hasher3 = DefaultHasher::new();
v2.hash(&mut hasher3);
let hash3 = hasher3.finish();
assert_eq!(v1, v2); assert_eq!(hash1, hash3);
let mut hasher4 = DefaultHasher::new();
v3.hash(&mut hasher4);
let hash4 = hasher4.finish();
assert_ne!(v1, v3);
assert_ne!(hash1, hash4);
let mut hasher5 = DefaultHasher::new();
let mut hasher6 = DefaultHasher::new();
v4.hash(&mut hasher5);
v5.hash(&mut hasher6);
let hash5 = hasher5.finish();
let hash6 = hasher6.finish();
assert_eq!(v4, v5); assert_eq!(hash5, hash6); assert_ne!(v4.uuid(), v5.uuid()); assert_ne!(v4.data, v5.data);
let test_cases: Vec<([f64; 2], [f64; 2])> = vec![
([0.0, 0.0], [0.0, 0.0]),
([1.0, 2.0], [1.0, 2.0]),
([-1.0, -2.0], [-1.0, -2.0]),
];
for (coords1, coords2) in test_cases {
let v_a: Vertex<f64, (), 2> = vertex!(coords1);
let v_b: Vertex<f64, (), 2> = vertex!(coords2);
assert_eq!(v_a, v_b);
let mut hasher_a = DefaultHasher::new();
let mut hasher_b = DefaultHasher::new();
v_a.hash(&mut hasher_a);
v_b.hash(&mut hasher_b);
assert_eq!(hasher_a.finish(), hasher_b.finish());
}
}
#[test]
fn test_vertex_collections() {
let mut set: FastHashSet<Vertex<f64, (), 2>> = FastHashSet::default();
let v1: Vertex<f64, (), 2> = vertex!([1.0, 2.0]);
let v2: Vertex<f64, (), 2> = vertex!([3.0, 4.0]);
let v3: Vertex<f64, (), 2> = vertex!([1.0, 2.0]);
assert!(set.insert(v1)); assert!(set.insert(v2)); assert!(!set.insert(v3));
assert_eq!(set.len(), 2);
assert!(set.contains(&v1));
assert!(set.contains(&v2));
assert!(set.contains(&v3));
let v4: Vertex<f64, (), 2> = vertex!([1.0, 2.0]);
assert!(set.contains(&v4));
let mut map: FastHashMap<Vertex<f64, (), 2>, i32> = FastHashMap::default();
let v5: Vertex<f64, (), 2> = vertex!([1.0, 2.0]);
let v6: Vertex<f64, (), 2> = vertex!([3.0, 4.0]);
map.insert(v5, 10);
map.insert(v6, 20);
assert_eq!(map.get(&v5), Some(&10));
assert_eq!(map.get(&v6), Some(&20));
assert_eq!(map.len(), 2);
let v7: Vertex<f64, (), 2> = vertex!([1.0, 2.0]);
assert_eq!(map.get(&v7), Some(&10));
let old_value = map.insert(v7, 30);
assert_eq!(old_value, Some(10)); assert_eq!(map.len(), 2); assert_eq!(map.get(&v5), Some(&30));
let v8: Vertex<f64, u16, 2> = vertex!([1.0, 2.0], 999u16);
let v9: Vertex<f64, i32, 2> = vertex!([3.0, 4.0], -42i32);
let mut map1: FastHashMap<Vertex<f64, u16, 2>, &str> = FastHashMap::default();
map1.insert(v8, "first");
assert_eq!(map1.len(), 1);
let mut map2: FastHashMap<Vertex<f64, i32, 2>, bool> = FastHashMap::default();
map2.insert(v9, true);
assert_eq!(map2.len(), 1);
}
macro_rules! test_vertex_dimensions {
($(
$test_name:ident => $dim:expr => [$($coord:expr),+ $(,)?]
),+ $(,)?) => {
$(
#[test]
fn $test_name() {
let vertex: Vertex<f64, (), $dim> = vertex!([$($coord),+]);
assert_vertex_properties(&vertex, [$($coord),+]);
assert!(vertex.data.is_none());
}
pastey::paste! {
#[test]
fn [<$test_name _with_data>]() {
let vertex: Vertex<f64, i32, $dim> = vertex!([$($coord),+], 42);
assert_vertex_properties(&vertex, [$($coord),+]);
assert_eq!(vertex.data, Some(42));
}
#[test]
fn [<$test_name _serialization_roundtrip>]() {
let vertex_with_data: Vertex<f64, i32, $dim> = vertex!([$($coord),+], 99);
let serialized = serde_json::to_string(&vertex_with_data).unwrap();
assert!(serialized.contains("\"data\":"));
let deserialized: Vertex<f64, i32, $dim> = serde_json::from_str(&serialized).unwrap();
assert_eq!(deserialized.data, Some(99));
assert_vertex_properties(&deserialized, [$($coord),+]);
let vertex_no_data: Vertex<f64, (), $dim> = vertex!([$($coord),+]);
let serialized = serde_json::to_string(&vertex_no_data).unwrap();
assert!(!serialized.contains("\"data\":"));
let deserialized: Vertex<f64, (), $dim> = serde_json::from_str(&serialized).unwrap();
assert_eq!(deserialized.data, None);
}
#[test]
fn [<$test_name _uuid_uniqueness>]() {
let v1: Vertex<f64, (), $dim> = vertex!([$($coord),+]);
let v2: Vertex<f64, (), $dim> = vertex!([$($coord),+]);
assert_ne!(v1.uuid(), v2.uuid());
assert!(!v1.uuid().is_nil());
assert!(!v2.uuid().is_nil());
}
}
)+
};
}
test_vertex_dimensions! {
vertex_2d => 2 => [1.0, 2.0],
vertex_3d => 3 => [1.0, 2.0, 3.0],
vertex_4d => 4 => [1.0, 2.0, 3.0, 4.0],
vertex_5d => 5 => [1.0, 2.0, 3.0, 4.0, 5.0],
}
#[test]
fn vertex_1d() {
let vertex: Vertex<f64, (), 1> = vertex!([42.0]);
assert_vertex_properties(&vertex, [42.0]);
assert!(vertex.data.is_none());
}
#[test]
fn test_vertex_data_types_and_ordering() {
let vertex_tuple: Vertex<f64, (i32, i32), 2> = vertex!([1.0, 2.0], (42, 84));
assert_vertex_properties(&vertex_tuple, [1.0, 2.0]);
assert_eq!(vertex_tuple.data.unwrap(), (42, 84));
let vertex_debug: Vertex<f64, i32, 3> = vertex!([1.0, 2.0, 3.0], 42);
let debug_str = format!("{vertex_debug:?}");
assert!(debug_str.contains("Vertex"));
assert!(debug_str.contains("point"));
assert!(debug_str.contains("uuid"));
assert!(debug_str.contains("1.0"));
assert!(debug_str.contains("2.0"));
assert!(debug_str.contains("3.0"));
let vertex1: Vertex<f64, (), 2> = vertex!([1.0, 2.0]);
let vertex2: Vertex<f64, (), 2> = vertex!([1.0, 2.0]);
assert!(vertex1.partial_cmp(&vertex2) != Some(Ordering::Less));
assert!(vertex2.partial_cmp(&vertex1) != Some(Ordering::Less));
assert!(matches!(
vertex1.partial_cmp(&vertex2),
Some(Ordering::Less | Ordering::Equal)
));
assert!(matches!(
vertex2.partial_cmp(&vertex1),
Some(Ordering::Less | Ordering::Equal)
));
assert!(matches!(
vertex1.partial_cmp(&vertex2),
Some(Ordering::Greater | Ordering::Equal)
));
assert!(matches!(
vertex2.partial_cmp(&vertex1),
Some(Ordering::Greater | Ordering::Equal)
));
}
#[test]
fn test_vertex_coordinate_values() {
let vertex_neg: Vertex<f64, (), 3> = vertex!([-1.0, -2.0, -3.0]);
assert_relative_eq!(
vertex_neg.point().coords().as_slice(),
[-1.0, -2.0, -3.0].as_slice(),
epsilon = 1e-9
);
assert_eq!(vertex_neg.dim(), 3);
let vertex_zero: Vertex<f64, (), 3> = vertex!([0.0, 0.0, 0.0]);
let origin_vertex: Vertex<f64, (), 3> = vertex!([0.0, 0.0, 0.0]);
assert_eq!(vertex_zero.point(), origin_vertex.point());
let vertex_large: Vertex<f64, (), 3> = vertex!([1e6, 2e6, 3e6]);
assert_relative_eq!(
vertex_large.point().coords().as_slice(),
[1_000_000.0, 2_000_000.0, 3_000_000.0].as_slice(),
epsilon = 1e-9
);
assert_eq!(vertex_large.dim(), 3);
let vertex_small: Vertex<f64, (), 3> = vertex!([1e-6, 2e-6, 3e-6]);
assert_relative_eq!(
vertex_small.point().coords().as_slice(),
[0.000_001, 0.000_002, 0.000_003].as_slice(),
epsilon = 1e-9
);
assert_eq!(vertex_small.dim(), 3);
let vertex_mixed: Vertex<f64, (), 4> = vertex!([1.0, -2.0, 3.0, -4.0]);
assert_relative_eq!(
vertex_mixed.point().coords().as_slice(),
[1.0, -2.0, 3.0, -4.0].as_slice(),
epsilon = 1e-9
);
assert_eq!(vertex_mixed.dim(), 4);
}
#[test]
fn test_vertex_properties() {
let vertex1: Vertex<f64, (), 3> = vertex!([1.0, 2.0, 3.0]);
let vertex2: Vertex<f64, (), 3> = vertex!([1.0, 2.0, 3.0]);
assert_ne!(vertex1.uuid(), vertex2.uuid());
assert!(!vertex1.uuid().is_nil());
assert!(!vertex2.uuid().is_nil());
}
#[test]
fn test_vertex_type_conversions() {
let vertex_coords: Vertex<f64, (), 3> = vertex!([1.0, 2.0, 3.0]);
let coords_owned: [f64; 3] = vertex_coords.into();
assert_relative_eq!(
coords_owned.as_slice(),
[1.0, 2.0, 3.0].as_slice(),
epsilon = 1e-9
);
let vertex_ref_coords: Vertex<f64, (), 3> = vertex!([4.0, 5.0, 6.0]);
let coords_ref: [f64; 3] = (&vertex_ref_coords).into();
assert_relative_eq!(
coords_ref.as_slice(),
[4.0, 5.0, 6.0].as_slice(),
epsilon = 1e-9
);
assert_relative_eq!(
vertex_ref_coords.point().coords().as_slice(),
[4.0, 5.0, 6.0].as_slice(),
epsilon = 1e-9
);
let vertex_point: Vertex<f64, (), 3> = vertex!([1.0, 2.0, 3.0]);
let point_from_vertex: Point<f64, 3> = (&vertex_point).into();
assert_relative_eq!(
point_from_vertex.coords().as_slice(),
[1.0, 2.0, 3.0].as_slice(),
epsilon = 1e-9
);
assert_eq!(point_from_vertex, *vertex_point.point());
assert_relative_eq!(
vertex_point.point().coords().as_slice(),
[1.0, 2.0, 3.0].as_slice(),
epsilon = 1e-9
);
let vertex_2d: Vertex<f64, (), 2> = vertex!([10.5, -5.3]);
let point_2d: Point<f64, 2> = (&vertex_2d).into();
assert_relative_eq!(
point_2d.coords().as_slice(),
[10.5, -5.3].as_slice(),
epsilon = 1e-9
);
assert_eq!(point_2d, *vertex_2d.point());
}
#[test]
#[expect(clippy::too_many_lines, reason = "Comprehensive validation test")]
fn test_vertex_validation() {
let valid_f64: Vertex<f64, (), 3> = vertex!([1.0, 2.0, 3.0]);
assert!(valid_f64.is_valid().is_ok());
let valid_f32: Vertex<f32, (), 2> = vertex!([1.5f32, 2.5f32]);
assert!(valid_f32.is_valid().is_ok());
let valid_negative: Vertex<f64, (), 3> = vertex!([-1.0, -2.0, -3.0]);
assert!(valid_negative.is_valid().is_ok());
let valid_zero: Vertex<f64, (), 3> = vertex!([0.0, 0.0, 0.0]);
assert!(valid_zero.is_valid().is_ok());
let valid_1d: Vertex<f64, (), 1> = vertex!([42.0]);
assert!(valid_1d.is_valid().is_ok());
let valid_5d: Vertex<f64, (), 5> = vertex!([1.0, 2.0, 3.0, 4.0, 5.0]);
assert!(valid_5d.is_valid().is_ok());
let invalid_nan_f64: Vertex<f64, (), 3> = Vertex {
point: Point::new([1.0, f64::NAN, 3.0]),
uuid: make_uuid(),
incident_cell: None,
data: None,
};
assert!(invalid_nan_f64.is_valid().is_err());
let invalid_all_nan: Vertex<f64, (), 3> = Vertex {
point: Point::new([f64::NAN, f64::NAN, f64::NAN]),
uuid: make_uuid(),
incident_cell: None,
data: None,
};
assert!(invalid_all_nan.is_valid().is_err());
let invalid_nan_f32: Vertex<f32, (), 2> = Vertex {
point: Point::new([1.0f32, f32::NAN]),
uuid: make_uuid(),
incident_cell: None,
data: None,
};
assert!(invalid_nan_f32.is_valid().is_err());
let invalid_1d_nan: Vertex<f64, (), 1> = Vertex {
point: Point::new([f64::NAN]),
uuid: make_uuid(),
incident_cell: None,
data: None,
};
assert!(invalid_1d_nan.is_valid().is_err());
let invalid_5d_nan: Vertex<f64, (), 5> = Vertex {
point: Point::new([1.0, 2.0, f64::NAN, 4.0, 5.0]),
uuid: make_uuid(),
incident_cell: None,
data: None,
};
assert!(invalid_5d_nan.is_valid().is_err());
let invalid_pos_inf: Vertex<f64, (), 3> = Vertex {
point: Point::new([1.0, f64::INFINITY, 3.0]),
uuid: make_uuid(),
incident_cell: None,
data: None,
};
assert!(invalid_pos_inf.is_valid().is_err());
let invalid_neg_inf: Vertex<f64, (), 3> = Vertex {
point: Point::new([1.0, f64::NEG_INFINITY, 3.0]),
uuid: make_uuid(),
incident_cell: None,
data: None,
};
assert!(invalid_neg_inf.is_valid().is_err());
let invalid_inf_f32: Vertex<f32, (), 2> = Vertex {
point: Point::new([f32::INFINITY, 2.0f32]),
uuid: make_uuid(),
incident_cell: None,
data: None,
};
assert!(invalid_inf_f32.is_valid().is_err());
let invalid_mixed: Vertex<f64, (), 3> = Vertex {
point: Point::new([f64::NAN, f64::INFINITY, 1.0]),
uuid: make_uuid(),
incident_cell: None,
data: None,
};
assert!(invalid_mixed.is_valid().is_err());
let valid_vertex: Vertex<f64, (), 3> = vertex!([1.0, 2.0, 3.0]);
assert!(valid_vertex.is_valid().is_ok());
assert!(!valid_vertex.uuid().is_nil());
let default_vertex: Vertex<f64, (), 3> = Vertex::empty();
match default_vertex.is_valid() {
Err(VertexValidationError::InvalidUuid { source: _ }) => (), other => panic!("Expected InvalidUuid error, got: {other:?}"),
}
assert!(default_vertex.uuid().is_nil());
assert!(default_vertex.point().validate().is_ok());
let invalid_uuid_vertex: Vertex<f64, (), 3> = Vertex {
point: Point::new([1.0, 2.0, 3.0]),
uuid: uuid::Uuid::nil(),
incident_cell: None,
data: None,
};
match invalid_uuid_vertex.is_valid() {
Err(VertexValidationError::InvalidUuid { source: _ }) => (), other => panic!("Expected InvalidUuid error, got: {other:?}"),
}
assert!(invalid_uuid_vertex.point().validate().is_ok());
assert!(invalid_uuid_vertex.uuid().is_nil());
let invalid_both: Vertex<f64, (), 3> = Vertex {
point: Point::new([f64::NAN, 2.0, 3.0]),
uuid: uuid::Uuid::nil(),
incident_cell: None,
data: None,
};
match invalid_both.is_valid() {
Err(VertexValidationError::InvalidPoint { .. }) => (), other => panic!("Expected InvalidPoint error, got: {other:?}"),
}
assert!(invalid_both.point().validate().is_err());
assert!(invalid_both.uuid().is_nil()); }
#[test]
fn vertex_string_data_usage_examples() {
let mut label_lookup: FastHashMap<u32, String> = FastHashMap::default();
label_lookup.insert(0, "center".to_string());
label_lookup.insert(1, "corner".to_string());
label_lookup.insert(2, "edge_midpoint".to_string());
label_lookup.insert(3, "boundary_point".to_string());
let vertices_with_ids: Vec<Vertex<f64, u32, 2>> = vec![
vertex!([0.5, 0.5], 0u32), vertex!([1.0, 1.0], 1u32), vertex!([0.5, 1.0], 2u32), vertex!([0.0, 0.5], 3u32), ];
for (i, v) in vertices_with_ids.iter().enumerate() {
let label_id = v.data.unwrap();
let label = label_lookup.get(&label_id).unwrap();
match i {
0 => assert_eq!(label, "center"),
1 => assert_eq!(label, "corner"),
2 => assert_eq!(label, "edge_midpoint"),
3 => assert_eq!(label, "boundary_point"),
_ => unreachable!(),
}
}
assert_eq!(vertices_with_ids.len(), 4);
let coords = vertices_with_ids[0].point().coords();
assert_abs_diff_eq!(coords[0], 0.5, epsilon = f64::EPSILON);
assert_abs_diff_eq!(coords[1], 0.5, epsilon = f64::EPSILON);
assert_eq!(vertices_with_ids[1].data.unwrap(), 1u32);
let vertex_set: FastHashSet<Vertex<f64, u32, 2>> =
vertices_with_ids.iter().copied().collect();
assert_eq!(vertex_set.len(), 4);
let vertices_with_chars: Vec<Vertex<f64, char, 2>> = vec![
vertex!([0.0, 0.0], 'A'),
vertex!([1.0, 0.0], 'B'),
vertex!([0.0, 1.0], 'C'),
];
for (i, v) in vertices_with_chars.iter().enumerate() {
let expected_char =
char::from(b'A' + usize_to_u8(i, 26).expect("Index should fit in u8"));
assert_eq!(v.data.unwrap(), expected_char);
}
let vertices_with_enums: Vec<Vertex<f64, PointType, 2>> = vec![
vertex!([0.0, 0.0], PointType::Origin),
vertex!([1.0, 0.0], PointType::Corner),
vertex!([0.5, 0.5], PointType::Interior),
];
assert_eq!(vertices_with_enums[0].data.unwrap(), PointType::Origin);
assert_eq!(vertices_with_enums[1].data.unwrap(), PointType::Corner);
assert_eq!(vertices_with_enums[2].data.unwrap(), PointType::Interior);
}
#[test]
fn vertex_hash_with_copy_data() {
let vertex1: Vertex<f64, u16, 2> = vertex!([1.0, 2.0], 999u16);
let vertex2: Vertex<f64, i32, 2> = vertex!([3.0, 4.0], 42);
let mut map: FastHashMap<Vertex<f64, u16, 2>, i32> = FastHashMap::default();
map.insert(vertex1, 100);
let mut map2: FastHashMap<Vertex<f64, i32, 2>, u8> = FastHashMap::default();
map2.insert(vertex2, 255u8);
assert_eq!(map.len(), 1);
assert_eq!(map2.len(), 1);
}
#[test]
#[expect(
clippy::too_many_lines,
reason = "Comprehensive deserialization edge-case test"
)]
fn test_vertex_deserialization_edge_cases() {
let json_minimal = r#"{
"point": [10.0, 20.0],
"uuid": "550e8400-e29b-41d4-a716-446655440000"
}"#;
let result: Result<Vertex<f64, (), 2>, _> = serde_json::from_str(json_minimal);
assert!(result.is_ok());
let vertex = result.unwrap();
assert_relative_eq!(
vertex.point().coords().as_slice(),
[10.0, 20.0].as_slice(),
epsilon = 1e-9
);
assert_eq!(
vertex.uuid().to_string(),
"550e8400-e29b-41d4-a716-446655440000"
);
assert!(vertex.incident_cell.is_none());
assert!(vertex.data.is_none());
let point = Point::new([1.5, 2.5, 3.5]);
let uuid_str = "550e8400-e29b-41d4-a716-446655440000";
let uuid = uuid::Uuid::parse_str(uuid_str).unwrap();
let cell_key = CellKey::from(KeyData::from_ffi(42u64));
let vertex_with_all = Vertex {
point,
uuid,
incident_cell: Some(cell_key),
data: Some(123i32),
};
assert_relative_eq!(
vertex_with_all.point().coords().as_slice(),
[1.5, 2.5, 3.5].as_slice(),
epsilon = 1e-9
);
assert_eq!(vertex_with_all.uuid().to_string(), uuid_str);
assert!(vertex_with_all.incident_cell.is_some());
assert_eq!(vertex_with_all.incident_cell.unwrap(), cell_key);
assert_eq!(vertex_with_all.data.unwrap(), 123);
let json_with_unknown = r#"{
"point": [1.0, 2.0, 3.0],
"uuid": "550e8400-e29b-41d4-a716-446655440000",
"incident_cell": null,
"data": null,
"unknown_field": "this should be ignored"
}"#;
let result: Result<Vertex<f64, (), 3>, _> = serde_json::from_str(json_with_unknown);
assert!(result.is_ok());
let vertex = result.unwrap();
assert_relative_eq!(
vertex.point().coords().as_slice(),
[1.0, 2.0, 3.0].as_slice(),
epsilon = 1e-9
);
let test_cases = vec![
(
r#"{"point": [1.0, 2.0, 3.0], "point": [4.0, 5.0, 6.0], "uuid": "550e8400-e29b-41d4-a716-446655440000"}"#,
"duplicate point",
),
(
r#"{"point": [1.0, 2.0, 3.0], "uuid": "550e8400-e29b-41d4-a716-446655440000", "uuid": "550e8400-e29b-41d4-a716-446655440001"}"#,
"duplicate uuid",
),
(
r#"{"point": [1.0, 2.0, 3.0], "uuid": "550e8400-e29b-41d4-a716-446655440000", "incident_cell": null, "incident_cell": "550e8400-e29b-41d4-a716-446655440001"}"#,
"duplicate incident_cell",
),
(
r#"{"point": [1.0, 2.0, 3.0], "uuid": "550e8400-e29b-41d4-a716-446655440000", "data": null, "data": null}"#,
"duplicate data",
),
(
r#"{"uuid": "550e8400-e29b-41d4-a716-446655440000"}"#,
"missing point",
),
(r#"{"point": [1.0, 2.0, 3.0]}"#, "missing uuid"),
];
for (json, description) in test_cases {
let result: Result<Vertex<f64, (), 3>, _> = serde_json::from_str(json);
assert!(
result.is_err(),
"Expected error for {description}, but got success"
);
let error_message = result.unwrap_err().to_string();
let has_relevant_error = error_message.contains("duplicate")
|| error_message.contains("missing")
|| error_message.contains("point")
|| error_message.contains("uuid")
|| error_message.contains("data")
|| error_message.contains("incident_cell");
assert!(
has_relevant_error,
"Error message for {description} doesn't contain expected keywords: {error_message}"
);
}
let invalid_json = r#"["not", "a", "vertex", "object"]"#;
let result: Result<Vertex<f64, (), 3>, _> = serde_json::from_str(invalid_json);
assert!(result.is_err());
let error_message = result.unwrap_err().to_string();
assert!(
error_message.contains("Vertex") || error_message.to_lowercase().contains("struct"),
"Error message should mention Vertex struct: {error_message}"
);
let vertex_with_nil_uuid = Vertex {
point: Point::new([1.0, 2.0, 3.0]),
uuid: uuid::Uuid::nil(),
incident_cell: None,
data: None::<()>,
};
let validation_result = vertex_with_nil_uuid.is_valid();
assert!(validation_result.is_err());
match validation_result.unwrap_err() {
VertexValidationError::InvalidUuid { source: _ } => (), other @ VertexValidationError::InvalidPoint { .. } => {
panic!("Expected InvalidUuid error, got: {other:?}")
}
}
}
#[test]
fn test_vertex_validation_error_display() {
let point_error =
crate::geometry::traits::coordinate::CoordinateValidationError::InvalidCoordinate {
coordinate_index: 1,
coordinate_value: "NaN".to_string(),
dimension: 3,
};
let vertex_error = VertexValidationError::InvalidPoint {
source: point_error,
};
let error_string = format!("{vertex_error}");
assert!(error_string.contains("Invalid point"));
let uuid_error = VertexValidationError::InvalidUuid {
source: UuidValidationError::NilUuid,
};
let uuid_error_string = format!("{uuid_error}");
assert!(uuid_error_string.contains("Invalid UUID"));
}
#[test]
fn test_vertex_validation_error_equality() {
let error1 = VertexValidationError::InvalidUuid {
source: UuidValidationError::NilUuid,
};
let error2 = VertexValidationError::InvalidUuid {
source: UuidValidationError::NilUuid,
};
assert_eq!(error1, error2);
let point_error =
crate::geometry::traits::coordinate::CoordinateValidationError::InvalidCoordinate {
coordinate_index: 1,
coordinate_value: "NaN".to_string(),
dimension: 3,
};
let error3 = VertexValidationError::InvalidPoint {
source: point_error.clone(),
};
let error4 = VertexValidationError::InvalidPoint {
source: point_error,
};
assert_eq!(error3, error4);
assert_ne!(error1, error3);
}
#[test]
fn test_set_uuid_valid() {
let mut vertex: Vertex<f64, (), 3> = vertex!([1.0, 2.0, 3.0]);
let original_uuid = vertex.uuid();
let new_uuid = make_uuid();
let result = vertex.set_uuid(new_uuid);
assert!(result.is_ok());
assert_eq!(vertex.uuid(), new_uuid);
assert_ne!(vertex.uuid(), original_uuid);
}
#[test]
fn test_set_uuid_nil_uuid() {
let mut vertex: Vertex<f64, (), 3> = vertex!([1.0, 2.0, 3.0]);
let original_uuid = vertex.uuid();
let result = vertex.set_uuid(uuid::Uuid::nil());
assert!(result.is_err());
assert_eq!(vertex.uuid(), original_uuid);
match result.unwrap_err() {
VertexValidationError::InvalidUuid {
source: UuidValidationError::NilUuid,
} => (), other => panic!("Expected InvalidUuid with NilUuid source, got: {other:?}"),
}
}
#[test]
fn test_set_uuid_invalid_version() {
let mut vertex: Vertex<f64, (), 3> = vertex!([1.0, 2.0, 3.0]);
let original_uuid = vertex.uuid();
let uuid_bytes = [0u8; 16]; let invalid_uuid = uuid::Uuid::from_bytes(uuid_bytes);
let result = vertex.set_uuid(invalid_uuid);
assert!(result.is_err());
assert_eq!(vertex.uuid(), original_uuid);
match result.unwrap_err() {
VertexValidationError::InvalidUuid { source: _ } => (), other @ VertexValidationError::InvalidPoint { .. } => {
panic!("Expected InvalidUuid error, got: {other:?}")
}
}
}
#[test]
fn test_serialization_deserialization_roundtrip() {
let original_vertex: Vertex<f64, char, 4> = vertex!([1.0, 2.0, 3.0, 4.0], 'A');
let serialized = serde_json::to_string(&original_vertex).unwrap();
let deserialized_vertex: Vertex<f64, char, 4> = serde_json::from_str(&serialized).unwrap();
assert_relative_eq!(
original_vertex.point().coords().as_slice(),
deserialized_vertex.point().coords().as_slice(),
epsilon = 1e-9
);
assert_eq!(original_vertex.uuid(), deserialized_vertex.uuid());
assert_eq!(
original_vertex.incident_cell,
deserialized_vertex.incident_cell
);
assert_eq!(original_vertex.data, deserialized_vertex.data);
}
#[test]
fn test_serialization_with_some_data_includes_field() {
let vertex: Vertex<f64, i32, 3> = vertex!([1.0, 2.0, 3.0], 42);
let serialized = serde_json::to_string(&vertex).unwrap();
assert!(
serialized.contains("\"data\":"),
"JSON should include data field when Some"
);
assert!(serialized.contains("42"), "JSON should include data value");
let deserialized: Vertex<f64, i32, 3> = serde_json::from_str(&serialized).unwrap();
assert_eq!(deserialized.data, Some(42));
assert_relative_eq!(
vertex.point().coords().as_slice(),
deserialized.point().coords().as_slice(),
epsilon = 1e-9
);
}
#[test]
fn test_serialization_with_none_data_omits_field() {
let vertex: Vertex<f64, (), 3> = vertex!([1.0, 2.0, 3.0]);
let serialized = serde_json::to_string(&vertex).unwrap();
assert!(
!serialized.contains("\"data\":"),
"JSON should omit data field when None"
);
let deserialized: Vertex<f64, (), 3> = serde_json::from_str(&serialized).unwrap();
assert_eq!(deserialized.data, None);
assert_relative_eq!(
vertex.point().coords().as_slice(),
deserialized.point().coords().as_slice(),
epsilon = 1e-9
);
}
#[test]
fn test_deserialization_with_explicit_null_data() {
let json_with_null =
r#"{"point":[1.0,2.0,3.0],"uuid":"550e8400-e29b-41d4-a716-446655440000","data":null}"#;
let vertex: Vertex<f64, (), 3> = serde_json::from_str(json_with_null).unwrap();
assert_eq!(vertex.data, None);
assert_relative_eq!(
vertex.point().coords().as_slice(),
[1.0, 2.0, 3.0].as_slice(),
epsilon = 1e-9
);
}
}