use std::io::Write;
use super::WriteError;
use super::entity::{HeaderEntity, WriterBody, WriterEntity};
use super::lexical::{format_enum, format_real, format_ref, format_string};
use crate::parser::entity::Attribute;
pub(super) fn serialize_attr(attr: &Attribute) -> Result<String, WriteError> {
match attr {
Attribute::Integer(n) => Ok(n.to_string()),
Attribute::Real(f) => format_real(*f),
Attribute::String(s) => Ok(format_string(s)),
Attribute::Enum(name) => Ok(format_enum(name)),
Attribute::Binary(hex) => Ok(format!("\"{hex}\"")),
Attribute::EntityRef(id) => Ok(format_ref(*id)),
Attribute::Unset => Ok("$".into()),
Attribute::Derived => Ok("*".into()),
Attribute::List(items) => {
let mut out = String::from("(");
for (i, item) in items.iter().enumerate() {
if i > 0 {
out.push(',');
}
out.push_str(&serialize_attr(item)?);
}
out.push(')');
Ok(out)
}
Attribute::Typed { type_name, value } => {
Ok(format!("{type_name}({})", serialize_attr(value)?))
}
}
}
fn serialize_attr_list(attrs: &[Attribute]) -> Result<String, WriteError> {
let mut out = String::from("(");
for (i, attr) in attrs.iter().enumerate() {
if i > 0 {
out.push(',');
}
out.push_str(&serialize_attr(attr)?);
}
out.push(')');
Ok(out)
}
pub(super) fn serialize_entity(entity: &WriterEntity) -> Result<String, WriteError> {
match &entity.body {
WriterBody::Simple { name, attrs } => Ok(format!(
"#{} = {name}{};",
entity.id,
serialize_attr_list(attrs)?
)),
WriterBody::Complex { parts } => {
let mut out = format!("#{} = (", entity.id);
for (i, (name, attrs)) in parts.iter().enumerate() {
if i > 0 {
out.push(' ');
}
out.push_str(name);
out.push_str(&serialize_attr_list(attrs)?);
}
out.push_str(");");
Ok(out)
}
}
}
pub(super) fn serialize_header_entity(entity: &HeaderEntity) -> Result<String, WriteError> {
Ok(format!(
"{}{};",
entity.name,
serialize_attr_list(&entity.attrs)?
))
}
pub(super) fn write_file<W: Write>(
writer: &mut W,
headers: &[HeaderEntity],
entities: &[WriterEntity],
) -> Result<(), WriteError> {
writer.write_all(b"ISO-10303-21;\n")?;
writer.write_all(b"HEADER;\n")?;
for h in headers {
writer.write_all(serialize_header_entity(h)?.as_bytes())?;
writer.write_all(b"\n")?;
}
writer.write_all(b"ENDSEC;\n")?;
writer.write_all(b"DATA;\n")?;
for e in entities {
writer.write_all(serialize_entity(e)?.as_bytes())?;
writer.write_all(b"\n")?;
}
writer.write_all(b"ENDSEC;\n")?;
writer.write_all(b"END-ISO-10303-21;\n")?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn integer_attr() {
assert_eq!(serialize_attr(&Attribute::Integer(7)).unwrap(), "7");
assert_eq!(serialize_attr(&Attribute::Integer(-42)).unwrap(), "-42");
}
#[test]
fn real_attr_rounds_through_lexical() {
assert_eq!(serialize_attr(&Attribute::Real(1.0)).unwrap(), "1.");
assert_eq!(serialize_attr(&Attribute::Real(1e-7)).unwrap(), "1.E-07");
}
#[test]
fn real_attr_surfaces_nan_error() {
assert!(matches!(
serialize_attr(&Attribute::Real(f64::NAN)),
Err(WriteError::InvalidFloat { .. })
));
}
#[test]
fn string_attr_escapes() {
assert_eq!(
serialize_attr(&Attribute::String("a'b".into())).unwrap(),
"'a''b'"
);
assert_eq!(
serialize_attr(&Attribute::String(String::new())).unwrap(),
"''"
);
}
#[test]
fn enum_attr() {
assert_eq!(
serialize_attr(&Attribute::Enum("MILLI".into())).unwrap(),
".MILLI."
);
}
#[test]
fn entity_ref_attr() {
assert_eq!(serialize_attr(&Attribute::EntityRef(7)).unwrap(), "#7");
}
#[test]
fn unset_and_derived_attrs() {
assert_eq!(serialize_attr(&Attribute::Unset).unwrap(), "$");
assert_eq!(serialize_attr(&Attribute::Derived).unwrap(), "*");
}
#[test]
fn list_attr_comma_separated() {
let list = Attribute::List(vec![
Attribute::Real(1.0),
Attribute::Real(2.0),
Attribute::Real(3.0),
]);
assert_eq!(serialize_attr(&list).unwrap(), "(1.,2.,3.)");
}
#[test]
fn nested_list_attr() {
let list = Attribute::List(vec![
Attribute::List(vec![Attribute::Integer(1), Attribute::Integer(2)]),
Attribute::EntityRef(9),
]);
assert_eq!(serialize_attr(&list).unwrap(), "((1,2),#9)");
}
#[test]
fn typed_attr() {
let typed = Attribute::Typed {
type_name: "LENGTH_MEASURE".into(),
value: Box::new(Attribute::Real(1e-7)),
};
assert_eq!(serialize_attr(&typed).unwrap(), "LENGTH_MEASURE(1.E-07)");
}
#[test]
fn simple_entity_renders_with_hash_and_semicolon() {
let e = WriterEntity {
id: 5,
body: WriterBody::Simple {
name: "CARTESIAN_POINT".into(),
attrs: vec![
Attribute::String(String::new()),
Attribute::List(vec![
Attribute::Real(1.0),
Attribute::Real(2.0),
Attribute::Real(3.0),
]),
],
},
};
assert_eq!(
serialize_entity(&e).unwrap(),
"#5 = CARTESIAN_POINT('',(1.,2.,3.));"
);
}
#[test]
fn complex_entity_renders_space_separated_parts() {
let e = WriterEntity {
id: 42,
body: WriterBody::Complex {
parts: vec![
("LENGTH_UNIT".into(), vec![]),
("NAMED_UNIT".into(), vec![Attribute::Derived]),
(
"SI_UNIT".into(),
vec![
Attribute::Enum("MILLI".into()),
Attribute::Enum("METRE".into()),
],
),
],
},
};
assert_eq!(
serialize_entity(&e).unwrap(),
"#42 = (LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.));"
);
}
#[test]
fn header_entity_omits_hash_prefix() {
let h = HeaderEntity {
name: "FILE_DESCRIPTION".into(),
attrs: vec![
Attribute::List(vec![Attribute::String("hello".into())]),
Attribute::String("2;1".into()),
],
};
assert_eq!(
serialize_header_entity(&h).unwrap(),
"FILE_DESCRIPTION(('hello'),'2;1');"
);
}
#[test]
fn empty_file_still_emits_wrapper() {
let mut buf = Vec::new();
write_file(&mut buf, &[], &[]).unwrap();
let text = String::from_utf8(buf).unwrap();
assert_eq!(
text,
"ISO-10303-21;\nHEADER;\nENDSEC;\nDATA;\nENDSEC;\nEND-ISO-10303-21;\n"
);
}
#[test]
fn file_renders_headers_then_entities() {
let headers = vec![HeaderEntity {
name: "FILE_SCHEMA".into(),
attrs: vec![Attribute::List(vec![Attribute::String("X".into())])],
}];
let entities = vec![WriterEntity {
id: 1,
body: WriterBody::Simple {
name: "POINT".into(),
attrs: vec![Attribute::Real(0.0)],
},
}];
let mut buf = Vec::new();
write_file(&mut buf, &headers, &entities).unwrap();
let text = String::from_utf8(buf).unwrap();
assert!(text.contains("FILE_SCHEMA(('X'));\n"));
assert!(text.contains("#1 = POINT(0.);\n"));
}
}