cadmpeg-codec-step 0.5.3

Read and write ISO 10303-21 STEP Part 21 files for AP203, AP214, and AP242.
Documentation
// SPDX-License-Identifier: Apache-2.0
//! External document and source dependency decoding.

use std::collections::{BTreeMap, BTreeSet, HashSet};

use cadmpeg_ir::report::LossNote;

use crate::loss::StepLossCode;
use crate::parse::{Exchange, RawRecord, Value};

use super::decode_text;
use super::StageOutcome;

pub(super) fn decode(exchange: &Exchange) -> StageOutcome<()> {
    let mut losses = Vec::new();
    let documents = exchange
        .records
        .iter()
        .filter_map(|(&id, record)| {
            let parameters = document_parameters(record)?;
            Some((
                id,
                (
                    parameters
                        .first()
                        .and_then(|value| {
                            decode_text(
                                exchange,
                                value,
                                &mut losses,
                                id,
                                "document identifier",
                                StepLossCode::MetadataStringInvalid,
                            )
                        })
                        .unwrap_or_default(),
                    parameters
                        .get(1)
                        .and_then(|value| {
                            decode_text(
                                exchange,
                                value,
                                &mut losses,
                                id,
                                "document name",
                                StepLossCode::MetadataStringInvalid,
                            )
                        })
                        .unwrap_or_default(),
                    parameters.get(3).and_then(ValueExt::reference),
                ),
            ))
        })
        .collect::<BTreeMap<_, _>>();
    let sources = exchange
        .records
        .iter()
        .filter_map(|(&id, record)| {
            let parameters = record.partial("EXTERNAL_SOURCE")?.parameters.as_slice();
            Some((
                id,
                parameters.first().and_then(|value| {
                    source_text(exchange, value, &mut losses, id, "external source")
                }),
            ))
        })
        .filter_map(|(id, source)| source.map(|source| (id, source)))
        .collect::<BTreeMap<_, _>>();
    let mut typed = HashSet::new();
    let mut notes = BTreeSet::new();

    for (&id, record) in &exchange.records {
        if let Some(parameters) = document_reference_parameters(record) {
            let Some(document_id) = parameters.first().and_then(ValueExt::reference) else {
                continue;
            };
            let Some((identifier, name, kind)) = documents.get(&document_id) else {
                continue;
            };
            let source = parameters
                .get(1)
                .and_then(|value| {
                    decode_text(
                        exchange,
                        value,
                        &mut losses,
                        id,
                        "document reference source",
                        StepLossCode::MetadataStringInvalid,
                    )
                })
                .unwrap_or_default();
            notes.insert(document_note(identifier, name, &source));
            typed.extend([id, document_id]);
            typed.extend(kind);
        }
        if let Some(partial) = record.partial("EXTERNALLY_DEFINED_ITEM") {
            let Some(source_id) = partial.parameters.get(1).and_then(ValueExt::reference) else {
                continue;
            };
            let Some(source) = sources.get(&source_id) else {
                continue;
            };
            let item = partial
                .parameters
                .first()
                .and_then(|value| source_text(exchange, value, &mut losses, id, "external item"))
                .unwrap_or_default();
            notes.insert(format!("external source {source} item {item}"));
            typed.extend([id, source_id]);
        }
    }

    StageOutcome {
        value: (),
        claims: typed,
        warnings: Vec::new(),
        notes: notes.into_iter().collect(),
        losses,
    }
}

fn document_parameters(record: &RawRecord) -> Option<&[Value]> {
    record
        .partial("DOCUMENT")
        .or_else(|| record.partial("DOCUMENT_FILE"))
        .map(|partial| partial.parameters.as_slice())
}

fn document_reference_parameters(record: &RawRecord) -> Option<&[Value]> {
    record
        .partial("DOCUMENT_REFERENCE")
        .or_else(|| record.partial("APPLIED_DOCUMENT_REFERENCE"))
        .map(|partial| partial.parameters.as_slice())
}

fn source_text(
    exchange: &Exchange,
    value: &Value,
    losses: &mut Vec<LossNote>,
    record_id: u64,
    field: &str,
) -> Option<String> {
    match value {
        Value::String(_) => decode_text(
            exchange,
            value,
            losses,
            record_id,
            field,
            StepLossCode::MetadataStringInvalid,
        ),
        Value::Typed(_, value) => source_text(exchange, value, losses, record_id, field),
        _ => None,
    }
}

fn document_note(identifier: &str, name: &str, source: &str) -> String {
    let identity = match (identifier.is_empty(), name.is_empty()) {
        (false, false) => format!("{identifier} ({name})"),
        (false, true) => identifier.to_owned(),
        (true, false) => name.to_owned(),
        (true, true) => "unnamed".to_owned(),
    };
    if source.is_empty() {
        format!("external document {identity}")
    } else {
        format!("external document {identity} from {source}")
    }
}

trait RecordExt {
    fn partial(&self, name: &str) -> Option<&crate::parse::PartialRecord>;
}

impl RecordExt for RawRecord {
    fn partial(&self, name: &str) -> Option<&crate::parse::PartialRecord> {
        self.partials.iter().find(|partial| partial.name == name)
    }
}

trait ValueExt {
    fn reference(&self) -> Option<u64>;
}

impl ValueExt for Value {
    fn reference(&self) -> Option<u64> {
        if let Value::Reference(id) = self {
            Some(*id)
        } else {
            None
        }
    }
}

#[cfg(test)]
pub(crate) mod tests;