ifc_lite_export/step_log/
merged.rs

1// SPDX-License-Identifier: MPL-2.0
2//! Merged export with per-model mutation logs (`MergedExporter.exportAsync`'s
3//! `bakeMutatedModels`): each model with pending edits is first written
4//! through the mutation-log writer in its own source schema, and the merge
5//! then treats the result as that model's source. Unit-aware federation,
6//! GlobalId reconciliation and id offsetting are therefore unchanged.
7//!
8//! The TypeScript bake passes no georeferencing edits to `StepExporter`, so a
9//! log's `georefMutations` are not applied to a merged export either; the
10//! merge says so in its warnings instead of dropping them silently.
11
12use std::io;
13
14use crate::merged::{export_merged_models, MergedModel, MergedOptions, MergedStats};
15use crate::step_api::StepOptions;
16
17use super::wire::MutationLog;
18use super::write::export_step_with_log;
19
20/// [`export_merged_models`] with a mutation log per model (`logs[i]` for
21/// `models[i]`; `None`, a missing entry or an empty log leaves the model as it
22/// is). Errs, writing nothing, when a model's log cannot be applied (see
23/// [`export_step_with_log`]).
24///
25/// A baked model is held in memory for the merge, as the merge itself holds its
26/// output; the models without edits are merged from their bytes directly.
27pub fn export_merged_models_with_logs(
28    models: &[MergedModel],
29    logs: &[Option<&MutationLog>],
30    opts: &MergedOptions,
31) -> io::Result<(String, MergedStats)> {
32    let mut baked: Vec<Option<Vec<u8>>> = Vec::with_capacity(models.len());
33    let mut georef_dropped = 0usize;
34    for (i, model) in models.iter().enumerate() {
35        let log = logs.get(i).copied().flatten();
36        let Some(log) = log.filter(|l| !l.mutations.is_empty() || !l.new_entities.is_empty()) else {
37            georef_dropped += usize::from(log.is_some_and(|l| l.georef_mutations.is_some()));
38            baked.push(None);
39            continue;
40        };
41        georef_dropped += usize::from(log.georef_mutations.is_some());
42        let mut edits = log.clone();
43        edits.georef_mutations = None;
44        let (text, _) = export_step_with_log(model.content, &StepOptions::default(), &edits)
45            .map_err(|e| io::Error::new(e.kind(), format!("model {i}: {e}")))?;
46        baked.push(Some(text.into_bytes()));
47    }
48    let inputs: Vec<MergedModel> = models
49        .iter()
50        .zip(&baked)
51        .map(|(model, bytes)| MergedModel {
52            content: bytes.as_deref().unwrap_or(model.content),
53            id: model.id.clone(),
54            included: model.included.clone(),
55        })
56        .collect();
57    let (out, mut stats) = export_merged_models(&inputs, opts);
58    if georef_dropped > 0 {
59        stats.warnings.push(format!(
60            "{georef_dropped} model log(s) carried georeferencing edits, which a merged export does not apply (as MergedExporter does not); export the model on its own to write them."
61        ));
62    }
63    Ok((out, stats))
64}