cadmpeg-codec-step 0.6.8

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_core::CodecError;
use cadmpeg_ir::report::loss::LossNote;

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

use super::decode_text;
use super::StageOutcome;
use super::{RecordExt, ValueExt};

pub(super) fn decode(exchange: &Exchange) -> Result<StageOutcome<()>, CodecError> {
    let mut losses = Vec::new();
    let mut documents = BTreeMap::new();
    let mut sources = BTreeMap::new();
    for (&id, record) in exchange.records() {
        if let Some(parameters) = document_parameters(record) {
            let identifier = parameters
                .first()
                .and_then(|value| {
                    decode_text(
                        exchange,
                        value,
                        &mut losses,
                        id,
                        "document identifier",
                        StepLossCode::MetadataStringInvalid,
                    )
                })
                .unwrap_or_default();
            let name = parameters
                .get(1)
                .and_then(|value| {
                    decode_text(
                        exchange,
                        value,
                        &mut losses,
                        id,
                        "document name",
                        StepLossCode::MetadataStringInvalid,
                    )
                })
                .unwrap_or_default();
            documents.insert(
                id,
                (
                    identifier,
                    name,
                    parameters.get(3).and_then(ValueExt::reference),
                ),
            );
        }
        if let Some(partial) = record.partial("EXTERNAL_SOURCE") {
            let parameters = partial.parameters.as_slice();
            if let Some(source) = parameters
                .first()
                .map(|value| source_text(exchange, value, &mut losses, id, "external source"))
                .transpose()?
                .flatten()
            {
                sources.insert(id, source);
            }
        }
    }
    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.insert(id);
            typed.insert(document_id);
            if let Some(kind) = kind {
                typed.insert(*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()
                .map(|value| source_text(exchange, value, &mut losses, id, "external item"))
                .transpose()?
                .flatten()
                .unwrap_or_default();
            notes.insert(["external source ", source, " item ", &item].join(""));
            typed.insert(id);
            typed.insert(source_id);
        }
    }

    let mut ordered_notes = Vec::new();
    ordered_notes.extend(notes);
    Ok(StageOutcome {
        value: (),
        claims: typed,
        notes: ordered_notes,
        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,
) -> Result<Option<String>, CodecError> {
    match value {
        Value::String(_) => Ok(decode_text(
            exchange,
            value,
            losses,
            record_id,
            field,
            StepLossCode::MetadataStringInvalid,
        )),
        Value::Typed(_, value) => source_text(exchange, value, losses, record_id, field),
        _ => Ok(None),
    }
}

fn document_note(identifier: &str, name: &str, source: &str) -> String {
    let identity: &[&str] = match (identifier.is_empty(), name.is_empty()) {
        (false, false) => &[identifier, " (", name, ")"],
        (false, true) => &[identifier],
        (true, false) => &[name],
        (true, true) => &["unnamed"],
    };
    let mut parts = Vec::new();
    parts.push("external document ");
    parts.extend_from_slice(identity);
    if !source.is_empty() {
        parts.extend([" from ", source]);
    }
    parts.concat()
}

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