cadmpeg-asm 0.6.6

Parse and write Autodesk Shape Manager record streams.
// SPDX-License-Identifier: Apache-2.0
//! Typed identity traversal for ASM-native record variants.

use super::{
    EndpointSlot, EvaluatedToleranceSlot, FaceContainment, FaceSidedness, TolerantCoedgeExtension,
    WireMembers, WireSide,
};
use cadmpeg_core::decode::DecodeContext;
use cadmpeg_core::CodecError;
use cadmpeg_ir::schema::rewrite::typed::{IdentityMap, RewriteIdentities};

macro_rules! scalar {
    ($($owner:ty),*) => {$(
        impl RewriteIdentities for $owner {
            fn visit_identity_references(&self, _ctx: &DecodeContext<'_>, _visitor: &mut dyn FnMut(&str) -> Result<(), CodecError>) -> Result<(), CodecError> { Ok(()) }
            fn rewrite_identities<F: FnMut(&str) -> Result<String, CodecError>>(self, _ctx: &DecodeContext<'_>, _map: &mut IdentityMap<'_, F>) -> Result<Self, CodecError> { Ok(self) }
        }
    )*};
}
scalar!(
    EndpointSlot,
    EvaluatedToleranceSlot,
    FaceContainment,
    TolerantCoedgeExtension,
    WireSide
);

impl RewriteIdentities for WireMembers {
    fn visit_identity_references(
        &self,
        ctx: &DecodeContext<'_>,
        visitor: &mut dyn FnMut(&str) -> Result<(), CodecError>,
    ) -> Result<(), CodecError> {
        let _depth = ctx.enter_nested("walk ASM wire members")?;

        match self {
            Self::Edges(edges) => edges.visit_identity_references(ctx, visitor),
            Self::Vertex(vertex) => vertex.visit_identity_references(ctx, visitor),
        }
    }
    fn rewrite_identities<F: FnMut(&str) -> Result<String, CodecError>>(
        self,
        ctx: &DecodeContext<'_>,
        map: &mut IdentityMap<'_, F>,
    ) -> Result<Self, CodecError> {
        let _depth = ctx.enter_nested("rewrite ASM wire members")?;

        Ok(match self {
            Self::Edges(edges) => Self::Edges(edges.rewrite_identities(ctx, map)?),
            Self::Vertex(vertex) => Self::Vertex(vertex.rewrite_identities(ctx, map)?),
        })
    }
}

impl RewriteIdentities for FaceSidedness {
    fn visit_identity_references(
        &self,
        ctx: &DecodeContext<'_>,
        visitor: &mut dyn FnMut(&str) -> Result<(), CodecError>,
    ) -> Result<(), CodecError> {
        let _depth = ctx.enter_nested("walk ASM face sidedness")?;

        self.source_namespace
            .visit("face-sidedness", self.record_index, visitor)?;
        self.face.visit_identity_references(ctx, visitor)
    }
    fn rewrite_identities<F: FnMut(&str) -> Result<String, CodecError>>(
        mut self,
        ctx: &DecodeContext<'_>,
        map: &mut IdentityMap<'_, F>,
    ) -> Result<Self, CodecError> {
        let _depth = ctx.enter_nested("rewrite ASM face sidedness")?;

        self.source_namespace =
            self.source_namespace
                .rewrite("face-sidedness", self.record_index, map)?;
        self.face = self.face.rewrite_identities(ctx, map)?;
        Ok(self)
    }
}

#[cfg(test)]
mod tests {
    use cadmpeg_core::decode::{DecodeArena, DecodeContext, DecodePolicy, ResourceDimension};
    use cadmpeg_core::CodecError;
    use cadmpeg_ir::schema::rewrite::typed::{IdentityMap, RewriteIdentities};

    fn record() -> super::super::EdgeContinuity {
        serde_json::from_value(serde_json::json!({"id":"f3d:asm:edge-continuity#1","record_index":1,"edge":"f3d:brep:entity#1","sense":"forward","continuity":"tangent"})).unwrap()
    }

    #[test]
    fn asm_typed_rewrite_preserves_native_record_wire() {
        let arena = DecodeArena::new();
        let (ctx, _) =
            DecodeContext::from_root_bytes(&[], &arena, &DecodePolicy::service()).unwrap();
        let mut map = IdentityMap::new(|source: &str| {
            Ok(format!(
                "f3d:brep/source/{}",
                source.strip_prefix("f3d:").unwrap()
            ))
        });
        let value = record().rewrite_identities(&ctx, &mut map).unwrap();
        assert_eq!(
            serde_json::to_value(value).unwrap(),
            serde_json::json!({"id":"f3d:brep/source/asm:edge-continuity#1","record_index":1,"edge":"f3d:brep/source/brep:entity#1","sense":"forward","continuity":"tangent"})
        );
        drop(map);
        ctx.finish_session().unwrap();
    }

    #[test]
    fn asm_typed_rewrite_preserves_the_depth_refusal() {
        let arena = DecodeArena::new();
        let mut policy = DecodePolicy::service();
        policy.limits.max_recursion_depth = 0;
        let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy).unwrap();
        let mut map = IdentityMap::new(|source: &str| Ok(source.to_owned()));
        let CodecError::ResourceLimit(limit) =
            record().rewrite_identities(&ctx, &mut map).unwrap_err()
        else {
            panic!("ASM rewrite must refuse");
        };
        assert_eq!(limit.dimension, ResourceDimension::RecursionDepth);
        drop(map);
        assert!(
            matches!(ctx.finish_session(), Err(CodecError::ResourceLimit(first)) if first == limit)
        );
    }

    #[test]
    fn asm_typed_rewrite_refuses_record_kind_or_index_changes() {
        for target in [
            "f3d:asm:edge-continuity#2",
            "f3d:asm:other-kind#1",
            "f3d:asm:edge-continuity#01",
        ] {
            let arena = DecodeArena::new();
            let (ctx, _) =
                DecodeContext::from_root_bytes(&[], &arena, &DecodePolicy::service()).unwrap();
            let mut map = IdentityMap::new(|_source: &str| Ok(target.to_owned()));
            let error = record().rewrite_identities(&ctx, &mut map).unwrap_err();
            assert!(
                matches!(error, CodecError::Malformed(message) if message == "ASM identity rewrite must preserve record kind and index")
            );
        }
    }
}