ifc_lite_export/
step_json.rs

1// SPDX-License-Identifier: MPL-2.0
2//! The mutation-JSON bridge: the wasm-facing entry point to the STEP writer.
3//!
4//! Its own module because it is its own contract. `step.rs` writes a model that
5//! is already in hand; this is the boundary a caller on the other side of the
6//! wasm ABI crosses, so it owns the payload shape, the rename attributes that
7//! spell it, and the fail-closed rule for a payload that does not parse.
8
9use serde::Deserialize;
10
11use crate::step::{export_step, AttrMutation, PropMutation, StepOptions};
12use crate::step_log::{export_step_with_log, MutationLog};
13
14
15#[derive(Deserialize)]
16struct AttrMutJson {
17    #[serde(rename = "expressId")]
18    express_id: u32,
19    index: usize,
20    value: String,
21}
22
23#[derive(Deserialize)]
24struct PropMutJson {
25    #[serde(rename = "expressId")]
26    express_id: u32,
27    #[serde(rename = "psetName")]
28    pset_name: String,
29    #[serde(rename = "propName")]
30    prop_name: String,
31    value: String,
32}
33
34#[derive(Deserialize, Default)]
35struct MutationsJson {
36    #[serde(default, rename = "attributeUpdates")]
37    attribute_updates: Vec<AttrMutJson>,
38    #[serde(default, rename = "propertyMutations")]
39    property_mutations: Vec<PropMutJson>,
40}
41
42/// Export STEP from raw bytes + a JSON mutation payload (the wasm bridge form of a
43/// `MutablePropertyView` diff).
44///
45/// Two payload shapes are accepted. A payload with a `mutations` array is the
46/// mutation LOG `MutablePropertyView.exportMutations()` writes (optionally with
47/// `newEntities` / `georefMutations`, see [`crate::MutationLog`]) and goes
48/// through [`crate::export_step_with_log`], the writer that applies the whole
49/// mutation vocabulary with byte parity to the TypeScript `StepExporter`
50/// (#5941). Any other object is the older pre-serialized shape below. A payload
51/// carrying both is refused: which set of edits it meant is not recoverable.
52///
53/// The older `mutations_json` shape:
54/// `{ "attributeUpdates": [{expressId,index,value}], "propertyMutations":
55/// [{expressId,psetName,propName,value}] }` where `value` is already STEP-serialized
56/// (`'Name'`, `IFCLABEL('x')`, `IFCREAL(1.)`). An empty string means "no mutations" —
57/// a legitimate, common case (plain re-export). A non-empty string that fails to
58/// parse is a caller bug (a malformed payload, a version mismatch across the wasm
59/// boundary) and must not be treated the same way: silently falling back to "no
60/// mutations" would export a file that LOOKS like a successful re-export of the
61/// user's edits but silently contains none of them. Callers get an `Err` instead,
62/// matching `exportGlb`'s and `exportMerged`'s fail-closed contract at this same
63/// wasm boundary.
64pub fn export_step_json(
65    content: &[u8],
66    schema: Option<String>,
67    included: Option<Vec<u32>>,
68    mutations_json: &str,
69) -> Result<String, String> {
70    if !mutations_json.trim().is_empty() {
71        let value: serde_json::Value =
72            serde_json::from_str(mutations_json).map_err(|e| format!("invalid mutations_json: {e}"))?;
73        if value.get("mutations").is_some() {
74            if value.get("attributeUpdates").is_some() || value.get("propertyMutations").is_some() {
75                return Err("invalid mutations_json: a mutation log (`mutations`) cannot be combined with \
76                            `attributeUpdates` / `propertyMutations`"
77                    .to_string());
78            }
79            let log: MutationLog =
80                serde_json::from_value(value).map_err(|e| format!("invalid mutations_json: {e}"))?;
81            let opts = StepOptions { schema, included, ..StepOptions::default() };
82            return export_step_with_log(content, &opts, &log).map(|(s, _)| s).map_err(|e| e.to_string());
83        }
84    }
85    let muts: MutationsJson = if mutations_json.trim().is_empty() {
86        MutationsJson::default()
87    } else {
88        serde_json::from_str(mutations_json)
89            .map_err(|e| format!("invalid mutations_json: {e}"))?
90    };
91    let opts = StepOptions {
92        schema,
93        included,
94        attribute_mutations: muts
95            .attribute_updates
96            .into_iter()
97            .map(|a| AttrMutation { express_id: a.express_id, index: a.index, value: a.value })
98            .collect(),
99        property_mutations: muts
100            .property_mutations
101            .into_iter()
102            .map(|p| PropMutation {
103                express_id: p.express_id,
104                pset_name: p.pset_name,
105                prop_name: p.prop_name,
106                value: p.value,
107            })
108            .collect(),
109        ..StepOptions::default()
110    };
111    export_step(content, &opts).map_err(|e| e.to_string())
112}