step_io/emit/mod.rs
1//! [`write()`] — the Part 21 envelope, the last step of writing.
2//!
3//! [`write()`] serializes a [`StepModel`] to Part 21 text. The `DATA`
4//! section comes from the generated writer; the `HEADER` section,
5//! `FILE_SCHEMA` included, comes from the model's own [`FileHeader`]
6//! ([`StepModel::header`]). Conformance is the authoring layer's job:
7//! [`crate::Ap242Author`] admits only AP242 entities and stamps the AP242
8//! schema identity on the header, so an authored model writes out as a
9//! valid AP242 file.
10
11use crate::generated::model::StepModel;
12use crate::generated::write::Writer;
13use crate::header::FileHeader;
14
15/// APD/`application_context` field values stamped by [`dump_universal`] so
16/// the dumped file's APD entities agree with its `FILE_SCHEMA` marker.
17struct ApdInfo {
18 status: &'static str,
19 name: &'static str,
20 year: i64,
21 description: &'static str,
22}
23
24/// Universal dump marker — step-io's all-AP union is not a real Application
25/// Protocol, so the `FILE_SCHEMA` header and APD declare a non-standard
26/// format. This keeps the dump internally consistent and prevents it
27/// masquerading as a real AP.
28const UNIVERSAL_FILE_SCHEMA: &str = "STEPIO_UNIVERSAL";
29static UNIVERSAL_APD: ApdInfo = ApdInfo {
30 status: "not a standard",
31 name: "stepio_universal",
32 year: 0,
33 description: "step-io universal union (non-standard, all-AP superset)",
34};
35
36/// Serialize the model to Part 21 text — the model's own header
37/// ([`StepModel::header`], `FILE_SCHEMA` included) plus every entity,
38/// verbatim.
39///
40/// An authored model ([`crate::StepBuilder`] / [`crate::Ap242Author`]) is
41/// AP242 by construction and carries the AP242 schema identity, so the
42/// output is a conforming AP242 file. A model that came from
43/// [`read`](crate::read) re-serializes faithfully under its *source* schema
44/// — step-io does not convert between APs.
45#[must_use]
46pub fn write(model: &StepModel) -> String {
47 wrap_envelope(&Writer::new(model).emit_all(), &model.header)
48}
49
50/// Dump the model as step-io's **Universal** union — every entity, verbatim,
51/// under the non-standard `STEPIO_UNIVERSAL` marker (`FILE_SCHEMA` and
52/// APD/`application_context` are set to it, keeping the dump internally
53/// consistent). Not a STEP output — the round-trip oracle for the external
54/// verification harness; the write path is [`write()`].
55#[doc(hidden)]
56#[must_use]
57pub fn dump_universal(model: &mut StepModel) -> String {
58 stamp_apd(model, &UNIVERSAL_APD);
59 model.header.schema =
60 crate::parser::schema::identify_schema(&[UNIVERSAL_FILE_SCHEMA.to_string()]);
61 write(model)
62}
63
64/// Absolutely set the model's APD entities (status/name/year) and
65/// `application_context` entities (application = `apd.description`) to `apd`,
66/// so the dumped file's APD agrees with the `FILE_SCHEMA` header.
67fn stamp_apd(model: &mut StepModel, apd: &ApdInfo) {
68 for x in &mut model.application_protocol_definition_arena.items {
69 x.status = apd.status.to_string();
70 x.application_interpreted_model_schema_name = apd.name.to_string();
71 x.application_protocol_year = apd.year;
72 }
73 for ac in &mut model.application_context_arena.items {
74 ac.application = apd.description.to_string();
75 }
76}
77
78/// Part 21 string literal for the HEADER section — same convention as the
79/// generated DATA writer's `step_str` (inner `'` doubled, non-ASCII passes
80/// through raw).
81fn header_str(s: &str) -> String {
82 let mut out = String::with_capacity(s.len() + 2);
83 out.push('\'');
84 for ch in s.chars() {
85 if ch == '\'' {
86 out.push('\'');
87 }
88 out.push(ch);
89 }
90 out.push('\'');
91 out
92}
93
94/// A HEADER string list; an empty list renders as the customary `('')`.
95fn header_list(items: &[String]) -> String {
96 if items.is_empty() {
97 return "('')".to_owned();
98 }
99 let inner = items
100 .iter()
101 .map(|s| header_str(s))
102 .collect::<Vec<_>>()
103 .join(",");
104 format!("({inner})")
105}
106
107/// Wrap a DATA body in the Part 21 envelope built from `header` —
108/// `FILE_SCHEMA` from [`FileHeader::schema`]'s raw strings (an absent raw
109/// renders as the customary empty `('')`).
110fn wrap_envelope(data_body: &str, header: &FileHeader) -> String {
111 let schema = header.schema.raw().map_or_else(
112 || "''".to_owned(),
113 |r| {
114 r.iter()
115 .map(|s| header_str(s))
116 .collect::<Vec<_>>()
117 .join(",")
118 },
119 );
120 format!(
121 "ISO-10303-21;\nHEADER;\nFILE_DESCRIPTION(({desc}),'2;1');\n\
122 FILE_NAME({name},{stamp},{authors},{orgs},{pre},{orig},{auth});\n\
123 FILE_SCHEMA(({schema}));\nENDSEC;\nDATA;\n{data_body}ENDSEC;\nEND-ISO-10303-21;\n",
124 desc = header_str(&header.description),
125 name = header_str(&header.file_name),
126 stamp = header_str(&header.time_stamp),
127 authors = header_list(&header.authors),
128 orgs = header_list(&header.organizations),
129 pre = header_str(&header.preprocessor_version),
130 orig = header_str(&header.originating_system),
131 auth = header_str(&header.authorisation),
132 )
133}