cadmpeg-ir 0.6.2

CAD data model, validation, diffing, and codec API for cadmpeg.
// SPDX-License-Identifier: Apache-2.0
#![allow(clippy::unwrap_used)]

#[cfg(feature = "schema")]
use crate::examples::unit_cube;
use crate::products::{ProductDefinition, ProductDefinitionKind};
use crate::report::check::Check;
use crate::validate::validate_neutral;
use crate::CadIr;

#[test]
fn typed_reference_walk_ignores_id_shaped_plain_strings() {
    let mut ir = crate::CadIr::empty();
    let owner =
        crate::ids::ProductDefinitionId::mint("test:model:product#owner").expect("valid identity");
    let target = crate::ids::BodyId::mint("test:model:body#missing").expect("valid identity");
    ir.model.product_definitions.push(ProductDefinition {
        id: owner.clone(),
        kind: ProductDefinitionKind::Part,
        source_name: Some("test:model:name#not-a-reference".into()),
        label: None,
        description: None,
        part_number: None,
        bom_properties: std::collections::BTreeMap::new(),
        bodies: vec![target.clone()],
        native_ref: None,
    });

    let mut references = Vec::new();
    crate::schema::EntitySchema::visit_references(
        &ir.model.product_definitions[0],
        &cadmpeg_test_support::service_decode_context(),
        &mut |reference| {
            references.push(reference.to_owned());
            Ok(())
        },
    )
    .expect("every entity states its typed references");
    assert_eq!(references, vec![target.as_str().to_owned()]);
    let mut borrowed_references = Vec::new();
    crate::schema::EntitySchema::visit_references(
        &ir.model.product_definitions[0],
        &cadmpeg_test_support::service_decode_context(),
        &mut |reference| {
            borrowed_references.push(reference.to_owned());
            Ok(())
        },
    )
    .expect("every entity states its typed references");
    assert_eq!(borrowed_references, references);

    let report = validate_neutral(&ir, Vec::new()).expect("validate the neutral model");
    assert!(report.findings.iter().any(|finding| {
        finding.check == Check::ReferentialIntegrity
            && finding.entity.as_deref() == Some(owner.as_str())
            && finding.message.contains(target.as_str())
    }));
    assert!(!report
        .findings
        .iter()
        .any(|finding| finding.message.contains("not-a-reference")));
}

#[test]
fn typed_reference_walk_treats_historical_members_as_state_local() {
    use crate::features::{
        edge_treatments::{FilletGroup, RadiusSpec},
        EdgeSelection, Feature, FeatureDefinition, FeatureId, FeatureInputTopology,
        FeatureOperation,
    };
    use crate::ids::{FeatureInputTopologyId, HistoricalEdgeId};
    use crate::schema::EntitySchema;

    let feature_id = FeatureId::mint("test:model:feature#owner").expect("identity grammar");
    let state_id =
        FeatureInputTopologyId::mint("test:model:feature-input#owner").expect("valid identity");
    let historical_edge =
        HistoricalEdgeId::mint("test:model:historical-edge#local").expect("valid identity");
    let state = FeatureInputTopology {
        id: state_id.clone(),
        input_of: feature_id.clone(),
        bodies: crate::features::DistinctMembers::try_from(Vec::new()).unwrap(),
        faces: crate::features::DistinctMembers::try_from(Vec::new()).unwrap(),
        edges: crate::features::DistinctMembers::try_from(vec![historical_edge.clone()]).unwrap(),
        vertices: crate::features::DistinctMembers::try_from(Vec::new()).unwrap(),
        native_ref: None,
    };
    let feature = Feature {
        id: feature_id.clone(),
        ordinal: 0,
        name: None,
        suppressed: None,
        dependencies: crate::features::DistinctMembers::default(),
        source_properties: std::collections::BTreeMap::new(),
        source_tag: None,
        source_text: None,
        source_content: crate::features::FeatureContent::default(),

        evaluation: crate::features::FeatureEvaluation::from_definition(
            FeatureDefinition::Operation(FeatureOperation::Fillet {
                groups: crate::features::NonEmptyMembers::one(FilletGroup {
                    edges: EdgeSelection::historical(
                        state_id.clone(),
                        vec![historical_edge],
                        "edge:local".into(),
                    )
                    .unwrap(),
                    radius: RadiusSpec::Constant {
                        radius: crate::scalar::PositiveLength::new(1.0).unwrap(),
                    },
                    tangency_weight: None,
                }),
            }),
        ),
        native_ref: None,
    };

    let mut state_references = Vec::new();
    state
        .visit_references(
            &cadmpeg_test_support::service_decode_context(),
            &mut |reference| {
                state_references.push(reference.to_owned());
                Ok(())
            },
        )
        .expect("state states its typed references");
    assert_eq!(state_references, vec![feature_id.as_str().to_owned()]);
    let ctx = cadmpeg_test_support::service_decode_context();
    let mut borrowed_state_references = Vec::new();
    state
        .visit_references(&ctx, &mut |target| {
            borrowed_state_references.push(target.to_owned());
            Ok(())
        })
        .unwrap();
    assert_eq!(borrowed_state_references, state_references);

    let mut feature_references = Vec::new();
    feature
        .visit_references(
            &cadmpeg_test_support::service_decode_context(),
            &mut |reference| {
                feature_references.push(reference.to_owned());
                Ok(())
            },
        )
        .expect("feature states its typed references");
    assert_eq!(feature_references, vec![state_id.as_str()]);
    let mut borrowed_feature_references = Vec::new();
    feature
        .visit_references(&ctx, &mut |target| {
            borrowed_feature_references.push(target.to_owned());
            Ok(())
        })
        .unwrap();
    assert_eq!(borrowed_feature_references, feature_references);

    let mut ir = CadIr::empty();
    ir.model.feature_input_topologies.push(state);
    ir.model.features.push(feature);
    assert!(!validate_neutral(&ir, Vec::new())
        .expect("validate the neutral model")
        .findings
        .iter()
        .any(|finding| finding.check == Check::ReferentialIntegrity));

    let missing = "test:model:historical-edge#missing";
    ir.model.features[0].evaluation.edit(|definition, _| {
        let FeatureDefinition::Operation(FeatureOperation::Fillet { groups }) = definition else {
            unreachable!("test feature is a fillet")
        };
        let EdgeSelection::Historical { edges, .. } = &mut groups[0].edges else {
            unreachable!("test fillet uses a historical selection")
        };
        *edges = vec![HistoricalEdgeId::mint(missing).expect("valid identity")]
            .try_into()
            .unwrap();
    });
    assert!(validate_neutral(&ir, Vec::new())
        .expect("validate the neutral model")
        .findings
        .iter()
        .any(|finding| {
            finding.check == Check::ReferentialIntegrity
                && finding.entity.as_deref() == Some(feature_id.as_str())
                && finding.message == format!("references missing historical edge `{missing}`")
        }));
}

#[cfg(feature = "schema")]
#[test]
fn schema_constrains_version_and_requires_subd_arena() {
    let schema = serde_json::to_value(crate::cadir_json_schema()).unwrap();
    let version_property = schema.pointer("/properties/ir_version").unwrap();
    let version_schema = match version_property["$ref"].as_str() {
        Some(reference) => schema
            .pointer(reference.strip_prefix('#').unwrap())
            .unwrap(),
        None => version_property,
    };
    assert_eq!(
        version_schema.get("const"),
        Some(&serde_json::json!(crate::IR_VERSION))
    );
    assert!(schema
        .pointer("/properties/model/$ref")
        .and_then(serde_json::Value::as_str)
        .is_some());

    let model_schema = schema.pointer("/$defs/Model").unwrap();
    assert!(model_schema
        .pointer("/required")
        .and_then(serde_json::Value::as_array)
        .unwrap()
        .contains(&serde_json::json!("subds")));
    assert!(schema.pointer("/properties/byte_ledger").is_none());

    let mut value = serde_json::to_value(unit_cube().expect("valid unit cube fixture")).unwrap();
    value
        .pointer_mut("/model")
        .unwrap()
        .as_object_mut()
        .unwrap()
        .remove("subds");
    assert!(serde_json::from_value::<CadIr>(value).is_err());
}

#[cfg(feature = "schema")]
#[test]
fn schema_generation_produces_definitions() {
    let schema = crate::cadir_json_schema();
    let json = serde_json::to_string(&schema).unwrap();
    assert!(json.contains("Body"));
    assert!(json.contains("Coedge"));
    assert!(json.contains("SurfaceGeometry"));
    let defs = schema
        .get("$defs")
        .and_then(serde_json::Value::as_object)
        .expect("schema has a $defs object");
    assert!(!defs.is_empty());
}

#[cfg(feature = "schema")]
#[test]
fn model_feature_schema_describes_serialized_regeneration_parent() {
    use crate::document::Model;
    use crate::features::{
        DistinctMembers, Feature, FeatureContent, FeatureDefinition, FeatureEvaluation, FeatureId,
        FeatureOperation, FeatureTreeNodeRole, TreeChildren,
    };

    let parent = FeatureId::mint("test:model:feature#parent").unwrap();
    let child = FeatureId::mint("test:model:feature#child").unwrap();
    let mut model = Model::default();
    model.features.push(Feature {
        id: parent.clone(),
        ordinal: 0,
        name: None,
        suppressed: None,
        dependencies: DistinctMembers::default(),
        source_properties: std::collections::BTreeMap::default(),
        source_tag: None,
        source_text: None,
        source_content: FeatureContent::default(),
        evaluation: FeatureEvaluation::from_definition(FeatureDefinition::Operation(
            FeatureOperation::TreeNode {
                role: FeatureTreeNodeRole::SolidBodies,
                children: TreeChildren::default(),
            },
        )),
        native_ref: None,
    });
    let mut child_feature = model.features[0].clone();
    child_feature.id = child.clone();
    child_feature.ordinal = 1;
    child_feature
        .evaluation
        .set_definition(FeatureDefinition::Operation(
            FeatureOperation::StoredGeometry {},
        ));
    model.features.push(child_feature);
    model
        .set_feature_regeneration_parent(&(child), &(parent.clone()))
        .unwrap();
    let wire = serde_json::to_value(&model).unwrap();
    assert_eq!(wire["features"][1]["regeneration_parent"], parent.as_str());

    let schema = serde_json::to_value(crate::cadir_json_schema()).unwrap();
    let row = schema
        .pointer("/$defs/Model/properties/features/items/$ref")
        .and_then(serde_json::Value::as_str)
        .and_then(|reference| reference.strip_prefix('#'))
        .and_then(|path| schema.pointer(path))
        .unwrap();
    assert!(row.pointer("/properties/regeneration_parent").is_some());

    let standalone = serde_json::to_value(schemars::schema_for!(Feature)).unwrap();
    assert!(standalone
        .pointer("/properties/regeneration_parent")
        .is_none());
}

#[test]
fn borrowed_reference_walk_refuses_at_the_depth_limit() {
    use crate::schema::EntitySchema;
    use cadmpeg_core::decode::{DecodeArena, DecodeContext, DecodePolicy, ResourceDimension};
    use cadmpeg_core::CodecError;
    let entity = ProductDefinition {
        id: "test:model:product#owner".try_into().unwrap(),
        kind: ProductDefinitionKind::Part,
        source_name: Some("test:model:name#not-a-reference".into()),
        label: None,
        description: None,
        part_number: None,
        bom_properties: std::collections::BTreeMap::new(),
        bodies: vec!["test:model:body#target".try_into().unwrap()],
        native_ref: None,
    };
    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 error = entity.visit_references(&ctx, &mut |_| Ok(())).unwrap_err();
    let CodecError::ResourceLimit(limit) = error else {
        panic!("borrowed walk refusal must remain a resource error");
    };
    assert_eq!(limit.dimension, ResourceDimension::RecursionDepth);
    assert!(
        matches!(ctx.finish_session(), Err(CodecError::ResourceLimit(original)) if original == limit)
    );
    let ctx = cadmpeg_test_support::service_decode_context();
    let mut seen = 0;
    entity
        .visit_references(&ctx, &mut |target| {
            assert_eq!(target, "test:model:body#target");
            seen += 1;
            Ok(())
        })
        .unwrap();
    assert_eq!(seen, 1);
}

#[test]
fn borrowed_reference_walk_preserves_a_callback_resource_refusal() {
    use crate::schema::EntitySchema;
    use cadmpeg_core::decode::{ResourceDimension, ResourceLimit};
    use cadmpeg_core::CodecError;
    let entity = crate::topology::Vertex {
        id: "test:model:vertex#owner".try_into().unwrap(),
        point: "test:model:point#target".try_into().unwrap(),
        tolerance: None,
    };
    let ctx = cadmpeg_test_support::service_decode_context();
    let limit = ResourceLimit::allocation_failed(
        ResourceDimension::RetainedBytes,
        4096,
        64,
        "borrowed reference callback",
    );
    let error = entity
        .visit_references(&ctx, &mut |_| Err(CodecError::ResourceLimit(limit)))
        .unwrap_err();
    assert!(matches!(error, CodecError::ResourceLimit(original) if original == limit));
}