Skip to main content

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}